diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..63ce98d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,39 @@ +# Django project +/media/ +/static/ +*.sqlite3 + +# Python and others +__pycache__ +*.pyc +.DS_Store +*.swp +/venv/ +/tmp/ +/.vagrant/ +/Vagrantfile.local +node_modules/ +/npm-debug.log +/.idea/ +.vscode +coverage +.python-version + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg diff --git a/.envs.example/.local/.django b/.envs.example/.local/.django new file mode 100644 index 0000000..481c5de --- /dev/null +++ b/.envs.example/.local/.django @@ -0,0 +1,22 @@ +# General +# ------------------------------------------------------------------------------ +USE_DOCKER=yes +IPYTHONDIR=/app/.ipython +# Redis +# ------------------------------------------------------------------------------ +REDIS_URL=redis://redis:6379/0 + +# Celery +# ------------------------------------------------------------------------------ + +# Flower +CELERY_FLOWER_USER=changeme +CELERY_FLOWER_PASSWORD=changeme + + +# Timeout fetch_data +# ------------------------------------------------------------------------------ +FETCH_DATA_TIMEOUT=2 + + +HF_TOKEN= diff --git a/.envs.example/.local/.postgres b/.envs.example/.local/.postgres new file mode 100644 index 0000000..ab3bbd7 --- /dev/null +++ b/.envs.example/.local/.postgres @@ -0,0 +1,7 @@ +# PostgreSQL +# ------------------------------------------------------------------------------ +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +POSTGRES_DB=scielo_core +POSTGRES_USER=changeme +POSTGRES_PASSWORD=changeme diff --git a/.envs.example/.production/.django b/.envs.example/.production/.django new file mode 100644 index 0000000..230fa19 --- /dev/null +++ b/.envs.example/.production/.django @@ -0,0 +1,25 @@ +# General +# ------------------------------------------------------------------------------ +USE_DOCKER=True +IPYTHONDIR=/app/.ipython +# Redis +# ------------------------------------------------------------------------------ +REDIS_URL=redis://redis:6379/0 + +# Celery +# ------------------------------------------------------------------------------ + +# Flower +CELERY_FLOWER_USER=changeme +CELERY_FLOWER_PASSWORD=changeme + +# Django +# ------------------------------------------------------------------------------ +DJANGO_SETTINGS_MODULE=config.settings.production +DJANGO_SECRET_KEY=changeme +DJANGO_ADMIN_URL=django-admin/ +DJANGO_ALLOWED_HOSTS=* + +# Sentry +# ------------------------------------------------------------------------------ +SENTRY_DSN= diff --git a/.envs.example/.production/.postgres b/.envs.example/.production/.postgres new file mode 100644 index 0000000..5e55a62 --- /dev/null +++ b/.envs.example/.production/.postgres @@ -0,0 +1,7 @@ +# PostgreSQL +# ------------------------------------------------------------------------------ +POSTGRES_HOST=postgres +POSTGRES_PORT=5432 +POSTGRES_DB=core +POSTGRES_USER=changeme +POSTGRES_PASSWORD=changeme diff --git a/.git-hook-commit-msg b/.git-hook-commit-msg new file mode 100755 index 0000000..4b0efdd --- /dev/null +++ b/.git-hook-commit-msg @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +"""Git commit-msg hook. + +Reads the activity (MarkAPI-NNNN) from the branch name -- anywhere in the +name -- and: + + 1. validates the message format: (): + allowed types: feature, fix, chore, unittest + 2. injects the activity prefix into the first line: [MarkAPI-NNNN] ... + +Automatic Git commits (merge/revert/squash) are skipped. +Uses the standard library only for portability across Linux and macOS. +""" + +import re +import subprocess +import sys + +ALLOWED_TYPES = ("feature", "fix", "chore", "unittest") + +BRANCH_ACTIVITY_RE = re.compile(r"MarkupAPI-(\d+)", re.IGNORECASE) +EXISTING_PREFIX_RE = re.compile(r"^\s*\[MarkupAPI-\d+\]\s*", re.IGNORECASE) +MESSAGE_FORMAT_RE = re.compile( + r"^(?:%s)(?:\([a-z0-9-]+\))?: .+" % "|".join(ALLOWED_TYPES) +) + +# Automatic commits generated by Git that should pass without validation. +MarkAPI_COMMIT_PREFIXES = ( + "Merge branch ", + "Merge pull request ", + "Merge remote-tracking ", + 'Revert "', +) +SQUASH_MERGE_RE = re.compile(r"^Merge .+ into ") + + +def fail(message): + print(message, file=sys.stderr) + sys.exit(1) + + +def is_auto_commit(first_line): + if first_line.startswith(MarkAPI_COMMIT_PREFIXES): + return True + return bool(SQUASH_MERGE_RE.match(first_line)) + + +def current_branch(): + # symbolic-ref works even on the first commit (branch with no commits), + # where rev-parse --abbrev-ref HEAD fails. On a detached HEAD, + # symbolic-ref returns a non-zero code and we fall back to rev-parse + # (which returns "HEAD"). + for cmd in ( + ["git", "symbolic-ref", "--short", "HEAD"], + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + ): + try: + result = subprocess.run(cmd, capture_output=True, text=True) + except OSError: + return None + if result.returncode == 0: + branch = result.stdout.strip() + if branch: + return branch + return None + + +def main(): + if len(sys.argv) < 2: + fail("❌ commit-msg hook: message file path not provided.") + + msg_path = sys.argv[1] + with open(msg_path, "r", encoding="utf-8") as handle: + lines = handle.read().splitlines() + + if not lines: + fail("❌ Empty commit message.") + + first_line = lines[0].rstrip("\r") + + # Let automatic Git commits pass through. + if is_auto_commit(first_line): + sys.exit(0) + + branch = current_branch() + if not branch or branch == "HEAD": + fail( + "❌ Could not determine the current branch (detached HEAD?).\n" + "The branch must contain the activity MarkAPI-NNNN." + ) + + match = BRANCH_ACTIVITY_RE.search(branch) + if not match: + fail( + "❌ Branch without an activity.\n" + "The branch name must contain MarkAPI-NNNN " + "(e.g. feature/MarkAPI-1234/my-feature).\n" + "Current branch: %s" % branch + ) + + activity = "MarkAPI-%s" % match.group(1) + + # Strip an existing prefix to avoid duplication (amend/rebase/reword). + body = EXISTING_PREFIX_RE.sub("", first_line).strip() + + if not MESSAGE_FORMAT_RE.match(body): + fail( + "❌ Invalid commit message.\n" + "Expected format: (): \n" + "Allowed types: %s\n" + "Examples:\n" + " chore: format readme\n" + " fix(auth): fix authentication when token is invalid" + % ", ".join(ALLOWED_TYPES) + ) + + lines[0] = "[%s] %s" % (activity, body) + + with open(msg_path, "w", encoding="utf-8") as handle: + handle.write("\n".join(lines) + "\n") + + sys.exit(0) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/nova-funcionalidade.md b/.github/ISSUE_TEMPLATE/nova-funcionalidade.md new file mode 100755 index 0000000..87bf256 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/nova-funcionalidade.md @@ -0,0 +1,85 @@ +--- +name: Nova funcionalidade +about: Contribua com novas idéias e necessidades +title: '' +labels: enhancement +assignees: '' + +--- + +### Descrição da nova funcionalidade +Eu, como **[tipo de cargo/ usuário / papel em sistema]**, gostaria que **[descrição breve da funcionalidade]**, então **[consequência ou o porque da requisição da atividade]**. + +### Critérios de aceitação + +Lista de critérios a serem observados pela equipe de engenharia durante a elaboração e construção da tarefa. Seja claro(a), descreva os pontos que são importantes para você: +- Ex 1: Fale sobre qual deve ser o comportamento da funcionalidade; +- Ex 2: Fale sobre quais validações um formulário deve conter; +- Ex 3: Fale sobre os tipos de impressão uma página deve suportar; +- Ex 4: Fale sobre os tipos de usuários podem realizar a ação requisitada; +- Critério 5; +- Critério 6 + +### Anexos +Este tópico é opcional mas pode ser utilizado para incluir objetos a serem analisados ou demonstrações que podem ser utilizados de exemplo. + +### Referências +Este tópico é opcional mas pode ser utilizado para enumerar items de referências como links ou bibliografia. + + +---- + +# Exemplos + +### 1) Descrição do requisito + +Como Usuário Administrador do OPAC, gostaria que o botão de publicação de periódicos possuisse **DESTAQUE**, assim poderia ter um indicativo visual de cuidado antes de clicar. + +### Critérios de aceitação + +Para que esta tarefa seja considerada concluída deve conter os seguintes pontos: +- O botão de publicação deve possuir um tom vermelho que se destaque dos outros elementos de tela; +- O botão deve conter o modo daltônico para que os membros daltônicos do time de publicação possam identifica-lo com facilidade; +- O botão deve ter conter um indicativo de "descrição de ação" ao posicionar o mouse e aguardar alguns segundos. + +### Anexos +N/A + +### Referências +N/A + +--- +### 2) Descrição do requisito +Como Usuário visitante do OPAC, gostaria que a página de artigos fosse adaptativa para celulares, assim poderia utilizar meu dispositivo móvel para navegar com mais facilidade. + +### Critérios de aceitação + +Os seguintes pontos devem ser contemplados: +- Os botões de navegação nesta tela devem ser de fácil acesso e possuir fácil toque; +- Os textos nesta tela devem possuir tamanho adequado para leitura seguindo os padrões da W3C; +- Deve-se agrupar em blocos as seções de página para facilitar a navegabilidade; + +### Anexos +N/A + +### Referências +N/A + +--- +### 3) Descrição do requisito +Como administrador do processo de qualidade, gostaria de ter um pré visualizador de HTML, assim poderia validar a marcação dos XMLs antes de envia-lo para publicação. + + +### Critérios de aceitação + +Os seguintes pontos devem ser contemplados: +- O visualizador de HTML deve ser auto contido e não depender de internet; +- O visualizador de HTML deve funcionar a partir do SPS 1.8; +- O visualizador de HTML deve exibir o conteúdo da mesma forma que o site oficial; +- O visualizador de HTML deve projetar as tabelas de forma correta; + +### Anexos +N/A + +### Referências +N/A diff --git a/.github/ISSUE_TEMPLATE/reportar-problema.md b/.github/ISSUE_TEMPLATE/reportar-problema.md new file mode 100755 index 0000000..abf5219 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/reportar-problema.md @@ -0,0 +1,38 @@ +--- +name: Reportar problema +about: Reporte um erro ou problema e nos ajude a melhorar nossos produtos +title: '' +labels: bug +assignees: '' + +--- + +### Descrição do problema +Descreva de forma clara e objetiva o problema relatado. + +### Passos para reproduzir o problema +1. Acesse a página ... +2. Clique no link ... +3. Role a página até ... +4. Observe o erro apresentado + +### Comportamento esperado +Descreva com clareza qual seria o comportamento **esperado** (correto) ao reproduzir os passos acima. + +### Screenshots ou vídeos +Para dar mais detalhes e contexto sobre o erro, considere anexar fotos ou vídeos do problema. + +### Anexos +Está seção é opcional, utilize para referenciar arquivos que servem de insumo para reproduzir o erro, ex: +- XML utilizado +- HTML produzido +- PDF criado + +### Ambiente utilizado + +Quando aplicável, forneça detalhes sobre o ambiente utilizado, ex: + +- Navegador Mozilla Firefox versão 30 +- Windows XP +- PC programs versão 1.0 +- Aparelho celular iPhone 7, iOS 7 diff --git a/.github/ISSUE_TEMPLATE/tarefa-de-desenvolvimento.md b/.github/ISSUE_TEMPLATE/tarefa-de-desenvolvimento.md new file mode 100755 index 0000000..a3ac269 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/tarefa-de-desenvolvimento.md @@ -0,0 +1,21 @@ +--- +name: Tarefa de desenvolvimento +about: Tarefas definidas pelo próprio time de desenvolvimento +title: '' +labels: task +assignees: '' + +--- + +### Descrição da tarefa +Descreva de forma clara e objetiva a tarefa em questão + +### Subtarefas + +- [ ] Descrição da primeira subtarefa +- [ ] Descrição da segunda subtarefa + + +## Considerações e notas + +* A implementação destas mudanças implica em aumentar o consumo de disco.. diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100755 index 0000000..7365d80 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,22 @@ +#### O que esse PR faz? +Fale sobre o propósito do pull request como por exemplo: quais problemas ele soluciona ou quais features ele adiciona. + +#### Onde a revisão poderia começar? +Indique o caminho do arquivo e o arquivo onde o revisor deve iniciar a leitura do código. + +#### Como este poderia ser testado manualmente? +Estabeleça os passos necessários para que a funcionalidade seja testada manualmente pelo revisor. + +#### Algum cenário de contexto que queira dar? +Indique um contexto onde as modificações se fazem necessárias ou passe informações que contextualizam +o revisor a fim de facilitar o entendimento da funcionalidade. + +### Screenshots +Quando aplicável e se fizer possível adicione screenshots que remetem a situação gráfica do problema que o pull request resolve. + +#### Quais são tickets relevantes? +Indique uma issue ao qual o pull request faz relacionamento. + +### Referências +Indique as referências utilizadas para a elaboração do pull request. + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100755 index 0000000..98a0de6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,79 @@ +# Config for Dependabot updates. See Documentation here: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + # Update GitHub actions in workflows + - package-ecosystem: "github-actions" + directory: "/" + # Check for updates to GitHub Actions every weekday + schedule: + interval: "daily" + + # Enable version updates for Docker + # We need to specify each Dockerfile in a separate entry because Dependabot doesn't + # support wildcards or recursively checking subdirectories. Check this issue for updates: + # https://github.com/dependabot/dependabot-core/issues/2178 + - package-ecosystem: "docker" + # Look for a `Dockerfile` in the `compose/local/django` directory + directory: "compose/local/django/" + # Check for updates to GitHub Actions every weekday + schedule: + interval: "daily" + + # Enable version updates for Docker + - package-ecosystem: "docker" + # Look for a `Dockerfile` in the `compose/local/docs` directory + directory: "compose/local/docs/" + # Check for updates to GitHub Actions every weekday + schedule: + interval: "daily" + + # Enable version updates for Docker + - package-ecosystem: "docker" + # Look for a `Dockerfile` in the `compose/local/node` directory + directory: "compose/local/node/" + # Check for updates to GitHub Actions every weekday + schedule: + interval: "daily" + + # Enable version updates for Docker + - package-ecosystem: "docker" + # Look for a `Dockerfile` in the `compose/production/aws` directory + directory: "compose/production/aws/" + # Check for updates to GitHub Actions every weekday + schedule: + interval: "daily" + + # Enable version updates for Docker + - package-ecosystem: "docker" + # Look for a `Dockerfile` in the `compose/production/django` directory + directory: "compose/production/django/" + # Check for updates to GitHub Actions every weekday + schedule: + interval: "daily" + + # Enable version updates for Docker + - package-ecosystem: "docker" + # Look for a `Dockerfile` in the `compose/production/postgres` directory + directory: "compose/production/postgres/" + # Check for updates to GitHub Actions every weekday + schedule: + interval: "daily" + + # Enable version updates for Docker + - package-ecosystem: "docker" + # Look for a `Dockerfile` in the `compose/production/traefik` directory + directory: "compose/production/traefik/" + # Check for updates to GitHub Actions every weekday + schedule: + interval: "daily" + + # Enable version updates for Python/Pip - Production + - package-ecosystem: "pip" + # Look for a `requirements.txt` in the `root` directory + # also 'setup.cfg', 'runtime.txt' and 'requirements/*.txt' + directory: "/" + # Check for updates to GitHub Actions every weekday + schedule: + interval: "daily" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100755 index 0000000..8d919df --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,63 @@ +name: CI + +# Enable Buildkit and let compose use it to speed up image building +env: + DOCKER_BUILDKIT: 1 + COMPOSE_DOCKER_CLI_BUILD: 1 + +on: + pull_request: + branches: [ "master", "main" ] + paths-ignore: [ "docs/**" ] + + push: + branches: [ "master", "main" ] + paths-ignore: [ "docs/**" ] + +concurrency: + group: ${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + linter: + runs-on: ubuntu-latest + steps: + + - name: Checkout Code Repository + uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + #cache: pip + #cache-dependency-path: | + # requirements/base.txt + # requirements/local.txt + + #- name: Run pre-commit + # uses: pre-commit/action@v3.0.0 + + # With no caching at all the entire ci process takes 4m 30s to complete! + tests: + runs-on: ubuntu-latest + + steps: + + - name: Checkout Code Repository + uses: actions/checkout@v6 + + - name: Build the Stack + run: docker compose -f local.yml build + + - name: Run DB Migrations + run: docker compose -f local.yml run --rm django python manage.py migrate + + - name: Run Django Tests + run: docker compose -f local.yml run --rm django python manage.py test --settings=config.settings.test + + - name: Run Pytest + run: docker compose -f local.yml run --rm django pytest + + - name: Tear down the Stack + run: docker compose -f local.yml down diff --git a/.gitignore b/.gitignore index 83972fa..cb2f1cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,218 +1,40 @@ -# Byte-compiled / optimized / DLL files +# Python __pycache__/ -*.py[codz] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -wheels/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py.cover -.hypothesis/ .pytest_cache/ -cover/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -.pybuilder/ -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -# Pipfile.lock - -# UV -# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# uv.lock - -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -# poetry.lock -# poetry.toml +*.pyc +*.pyo +*.pyd -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. -# https://pdm-project.org/en/latest/usage/project/#working-with-version-control -# pdm.lock -# pdm.toml -.pdm-python -.pdm-build/ - -# pixi -# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. -# pixi.lock -# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one -# in the .venv directory. It is recommended not to include this directory in version control. -.pixi - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# Redis -*.rdb -*.aof -*.pid - -# RabbitMQ -mnesia/ -rabbitmq/ -rabbitmq-data/ - -# ActiveMQ -activemq-data/ - -# SageMath parsed files -*.sage.py - -# Environments -.env -.envrc -.venv -env/ +# Environmental variables venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site +.venv/ +.envs +.envs.local +*.envs -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -# .idea/ - -# Abstra -# Abstra is an AI-powered process automation framework. -# Ignore directories containing user credentials, local state, and settings. -# Learn more at https://abstra.io/docs -.abstra/ - -# Visual Studio Code -# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore -# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore -# and can be added to the global gitignore or merged into this file. However, if you prefer, -# you could uncomment the following to ignore the entire vscode folder -# .vscode/ -# Temporary file for partial code execution -tempCodeRunnerFile.py +# Logs +*.log -# Ruff stuff: -.ruff_cache/ +# Editor +.vscode/ +.idea/ +*.swp +*.swo +*.tmp -# PyPI configuration file -.pypirc +# Build artifacts +dist/ +build/ +.coverage +coverage/ +htmlcov/ -# Marimo -marimo/_static/ -marimo/_lsp/ -__marimo__/ +# OS +.DS_Store +Thumbs.db -# Streamlit -.streamlit/secrets.toml +# Data data +core/media +/ai/download +.ai +.ipython diff --git a/Makefile b/Makefile new file mode 100755 index 0000000..55c0dc3 --- /dev/null +++ b/Makefile @@ -0,0 +1,144 @@ +COMPOSE_FILE ?= local.yml + +export SCMS_BUILD_DATE := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ") +export SCMS_VCS_REF := $(shell git rev-parse --short HEAD) +export SCMS_WEBAPP_VERSION := $(shell git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0") + +default: build + +help: ## Show this help + @echo 'Usage: make [target] [COMPOSE_FILE=file]' + @echo '' + @echo 'Targets:' + @egrep '^(.+)\:\ .*##\ (.+)' ${MAKEFILE_LIST} | sed 's/:.*##/#/' | column -t -c 1 -s "#" + @echo '' + +app_version: ## Show version of webapp + @echo "Version: $(SCMS_WEBAPP_VERSION)" + +latest_commit: ## Show last commit ref + @echo "Latest commit: $(SCMS_VCS_REF)" + +build_date: ## Show build date + @echo "Build date: $(SCMS_BUILD_DATE)" + +configure_git_hooks: ## Configure git hooks + cp -fa .git-hook-commit-msg .git/hooks/commit-msg + ln -sf ../../.git-hook-commit-msg .git/hooks/commit-msg + +############################################ +## docker compose shortcuts +############################################ + +build: ## Build app using $(COMPOSE_FILE) + docker compose -f $(COMPOSE_FILE) build + +build_no_cache: ## Build app without cache + docker compose -f $(COMPOSE_FILE) build --no-cache + +build_llama: ## Build app with llama-cpp-python support + DOCKERFILE=./compose/local/django/Dockerfile.llama docker compose -f $(COMPOSE_FILE) build + +up: ## Start app + docker compose -f $(COMPOSE_FILE) up -d + +logs: ## See all app logs + docker compose -f $(COMPOSE_FILE) logs -f + +stop: ## Stop all services + docker compose -f $(COMPOSE_FILE) stop + +restart: ## Restart all services + docker compose -f $(COMPOSE_FILE) restart + +ps: ## List containers + docker compose -f $(COMPOSE_FILE) ps + +rm: ## Remove all containers + docker compose -f $(COMPOSE_FILE) rm -f + +django_shell: ## Open Django shell + docker compose -f $(COMPOSE_FILE) run --rm django python manage.py shell + +django_createsuperuser: ## Create a superuser + docker compose -f $(COMPOSE_FILE) run --rm django python manage.py createsuperuser + +django_bash: ## Open bash in django container + docker compose -f $(COMPOSE_FILE) run --rm django bash + +test: ## Run tests (pytest default) + docker compose -f $(COMPOSE_FILE) run --rm django pytest --reuse-db + +test-fast: ## Run tests (pytest failfast) + docker compose -f $(COMPOSE_FILE) run --rm django pytest -x --reuse-db + +test-cov: ## Run tests with coverage + docker compose -f $(COMPOSE_FILE) run --rm django pytest --reuse-db --cov=manuscripts --cov-report=term-missing manuscripts/tests + +test-fresh: ## Recreate test database and run pytest + docker compose -f $(COMPOSE_FILE) run --rm django pytest --create-db + +django_test: ## Run tests (legacy Django runner) + docker compose -f $(COMPOSE_FILE) run --rm django python manage.py test --settings=config.settings.test + +django_fast: ## Run tests (legacy Django runner failfast) + docker compose -f $(COMPOSE_FILE) run --rm django python manage.py test --settings=config.settings.test --failfast + +pytest: test ## Alias: pytest default + +pytest_fast: test-fast ## Alias: pytest failfast + +pytest_cov: test-cov ## Alias: pytest coverage + +django_makemigrations: ## Run makemigrations + docker compose -f $(COMPOSE_FILE) run --rm django python manage.py makemigrations + +django_migrate: ## Run migrate + docker compose -f $(COMPOSE_FILE) run --rm django python manage.py migrate + +django_makemessages: ## Run makemessages + docker compose -f $(COMPOSE_FILE) run --rm django python manage.py makemessages --all + +django_compilemessages: ## Run compilemessages + docker compose -f $(COMPOSE_FILE) run --rm django python manage.py compilemessages + +wagtail_sync: ## Sync wagtail page translation fields + docker compose -f $(COMPOSE_FILE) run --rm django python manage.py sync_page_translation_fields + +wagtail_update_translation_field: ## Update wagtail translation fields + docker compose -f $(COMPOSE_FILE) run --rm django python manage.py update_translation_fields + +django_dump_auth: ## Dump auth data to fixtures/auth.json + docker compose -f $(COMPOSE_FILE) run --rm django python manage.py dumpdata auth --indent=2 --output=fixtures/auth.json + +django_load_auth: ## Load auth data from fixtures/auth.json + docker compose -f $(COMPOSE_FILE) run --rm django python manage.py loaddata --database=default fixtures/auth.json + +dump_data: ## Dump database into timestamped .sql file + docker compose -f $(COMPOSE_FILE) exec postgres pg_dumpall -c -U debug > dump_$$(date +%d-%m-%Y"_"%H_%M_%S).sql + +restore_data: ## Restore database from backup/latest.sql + docker compose -f $(COMPOSE_FILE) exec -T postgres psql -U debug < backup/latest.sql + +volume_down: ## Remove all volumes + docker compose -f $(COMPOSE_FILE) down -v + +############################################ +## Cleanup +############################################ + +clean_migrations: ## Remove all migration files + find . -path "*/migrations/*.py" -not -name "__init__.py" -delete + find . -path "*/migrations/*.pyc" -delete + +clean_container: ## Remove all containers + docker rm $$(docker ps -a -q --no-trunc) + +clean_dangling_images: ## Remove dangling images + docker image prune -f + +clean_dangling_volumes: ## Remove dangling volumes + docker volume rm $$(docker volume ls -f dangling=true -q) + +clean_project_images: ## Remove project images + docker rmi -f $$(docker images --filter=reference='*scielo_tools*' -q) diff --git a/README.md b/README.md index 66c5b71..91b45cf 100644 --- a/README.md +++ b/README.md @@ -1 +1,257 @@ -# scielo-tools \ No newline at end of file +# MarkAPI + +Plataforma web para produção, validação e rastreio de artigos acadêmicos em formato **SPS XML**. Transforma DOCX em XML SPS com apoio de IA, gera HTML e PDF, valida pacotes SPS e expõe API REST para integração. + +Stack: Python 3.12, Django 6.0, Wagtail 7.4, DRF, Celery, Redis, PostgreSQL, packtools. + +--- + +## Melhorias futuras + +### Arquitetura e organização +- **Dividir `manuscripts` em apps menores.** O app concentra pipeline, estrutura, modelos, views e admin. Separar em `ingestion` (upload, inspeção), `pipeline` (ações de processamento) e `articles` (modelos Article + StructureVersion + Artifact) reduziria o acoplamento e o tamanho dos módulos. +- **Extrair `utils/xml_utils.py` para o app `sps`.** As funções `parse_xml_structure`, `extract_article_metadata` e `generate_structure_xml` são operações de parsing/emissão XML SPS e naturalmente pertencem a `sps/`, não a `manuscripts/utils/`. +- **Revisar `ai/prompts/`.** Consolidar `vision.py` e `text.py` em um único módulo com prompts parametrizáveis por modalidade, evitando duplicação de lógica entre as duas trilhas. + +### Funcionalidades +- **Pipeline assíncrono por ação.** Hoje `process_input` executa todas as ações sequencialmente. Cada ação poderia ser uma Celery task encadeada, permitindo retry individual e visibilidade granular. +- **Cache de resultados LLM.** O frontmatter extraído por LLM poderia ser cacheado por checksum do conteúdo, evitando re-chamadas ao reprocessar o mesmo documento. +- **Validação estrutural precoce.** Validar estrutura do DOCX (estilos, seções obrigatórias) antes de iniciar o pipeline, reduzindo falhas tardias. + +### Dívida técnica +- **Mover `labeling/` para `utils/`.** O módulo `labeling` só contém funções utilitárias de segmentação e rotulagem — não é um app Django propriamente dito. +- **Remover referências a nomes antigos.** Código e configurações ainda referenciam `scielo_tools` e `markup_doc` — padronizar para `markapi`. +- **Testes de integração para o pipeline completo.** Hoje os testes cobrem unidades isoladas. Adicionar testes end-to-end com fixtures de DOCX → XML → validação → HTML → PDF. + +### Infraestrutura +- **Suporte a GPU no container LLM.** O Dockerfile `Dockerfile.llama` hoje depende de CPU. Adicionar variante com CUDA para ambientes com GPU. +- **Métricas e observabilidade.** Expor métricas de uso do pipeline (taxa de sucesso por ação, latência, consumo de tokens LLM) via Prometheus/Grafana. + +--- + +## Apps + +| App | Descrição | +|-----|-----------| +| `manuscripts` | Pipeline de processamento — upload, ingestão, inspeção, estrutura, artefatos | +| `sps` | Geração e validação de XML SPS, HTML e PDF via packtools + LibreOffice | +| `ai` | Modelos LLM (Gemini, Ollama, HuggingFace), serviço unificado, prompts | +| `labeling` | Rotulagem de conteúdo DOCX, segmentação de seções, citações | +| `references` | Referências bibliográficas, deduplicação, parsing via IA, API REST | +| `journals` | Periódicos e issues — sincronização com Core API | +| `docx_parser` | Biblioteca de extração de conteúdo e estrutura de arquivos DOCX | +| `core` | Modelos base (`CommonControlField`), choices (línguas), forms, requester | +| `core_settings` | Configurações editáveis do site (nome, logo, favicon) via Wagtail | +| `users` | `CustomUser` (AUTH_USER_MODEL) | + +### Estrutura do app `manuscripts` + +``` +manuscripts/ +├── controller.py lógica de negócio (artigos + eventos) +├── artifacts.py salvamento e gerenciamento de artefatos + assets +├── blocks.py StreamField blocks do Wagtail +├── choices.py enums (ProcessStatus, InputType, ArtifactType, …) +├── forms.py formulários de upload e revisão +├── processing.py orquestrador do pipeline (process_input) +├── structure.py persistência de estrutura e referências no DB +├── tasks.py tarefas Celery +├── wagtail_hooks.py viewsets, registro de snippets, hooks do admin +├── models/ +│ ├── article.py Article, StructureVersion, Reference, Citation, Artifact +│ └── processing.py Processing, ProcessingEvent, proxies +├── utils/ +│ ├── docx_utils.py extração de estrutura de DOCX +│ ├── frontmatter.py enriquecimento de frontmatter via payload LLM +│ ├── helpers.py checksum, json_safe, blocks, safe_archive_members +│ ├── ingestion.py ingestão de documentos, XMLs e ZIPs +│ ├── inspection.py inspeção de entrada e resolução de ações +│ ├── processing_actions.py handlers por ação (citation, xml, html, pdf, …) +│ └── xml_utils.py parsing e geração de XML SPS +└── views/ + ├── editorial.py views de edição e revisão + └── wagtail.py views de criação e inspeção do Wagtail +``` + +--- + +## Pipeline de processamento + +``` +1. Upload + O usuário envia DOCX, XML ou ZIP via Wagtail admin. + +2. Inspeção (inspect_processing) + Detecta o tipo de entrada, sugere ações aplicáveis. + +3. Ingestão (process_input → _ingest_input) + Extrai o documento ou XML do ZIP, associa a um Article, + extrai assets embutidos (imagens do DOCX). + +4. Marcação de citações (CITATION_MARKUP) + Identifica e marca referências no DOCX via sps.xref. + Extrai estrutura (front/body/back). + Envia frontmatter ao LLM para extração de metadados. + Cria ArticleStructureVersion com blocos estruturados. + +5. Geração XML (XML_GENERATION) + Converte a estrutura em XML SPS via sps.xml. + Preserva nós não-modelados via round-trip com XML base. + +6. Validação XML (XML_VALIDATION) + Valida o XML contra SPS via packtools. + +7. Geração de pacote SPS (SPS_PACKAGE_GENERATION) + Empacota XML + assets referenciados em ZIP. + +8. Geração HTML (HTML_GENERATION) + Renderiza HTML a partir do XML SPS. + +9. Geração PDF (PDF_GENERATION) + Converte XML → DOCX (packtools) → PDF (LibreOffice). +``` + +Cada ação é registrada como `ProcessingEvent` (status, mensagem, detalhes) +e produz `ArticleArtifact` versionado (is_current / is_stale). + +--- + +## Desenvolvimento + +**Pré-requisitos:** Docker, Docker Compose, Make. + +### Primeira execução + +```bash +make build +make up +make django_migrate +make django_createsuperuser +``` + +### Serviços locais + +| Serviço | URL | +|---------|-----| +| Wagtail/Django | http://127.0.0.1:8009 | +| MailHog | http://127.0.0.1:8029 | +| PostgreSQL | localhost:5439 | +| Redis | localhost:6399 | +| Flower | http://127.0.0.1:5559 | + +```bash +make help # todos os alvos Make +``` + +### Compose + +Arquivo principal: `local.yml` (dev). Ambiente em `.envs/.local/`. + +Volume Postgres em `../scms_data/scielo_tools/data_dev`. + +```bash +make build COMPOSE_FILE=local.yml +make up +make logs +``` + +### Variante LLM local (llama.cpp) + +```bash +make build_llama +``` + +Define `DOCKERFILE=./compose/local/django/Dockerfile.llama` antes do build. +O compose e demais comandos continuam usando `local.yml`. + +### Modelo LLM + +[Wiki — baixar e configurar modelo](https://github.com/scieloorg/scielo-tools/wiki) + +--- + +## Testes + +```bash +make test # pytest (padrão, reutiliza DB de teste) +make test-fast # pytest com -x +make test-cov # pytest com coverage (manuscripts, 100%) +make test-fresh # recria o banco de teste e roda pytest + +# aliases (compatibilidade) +make pytest +make pytest_fast +make pytest_cov + +# legado (Django runner) +make django_test # manage.py test --settings=config.settings.test +make django_fast # com --failfast +``` + +--- + +## Configuração + +### Settings modules + +| `DJANGO_SETTINGS_MODULE` | Uso | +|--------------------------|-----| +| `config.settings.local` | Desenvolvimento (default) | +| `config.settings.production` | Produção | +| `config.settings.test` | Testes | + +### Arquivos de ambiente + +- `.envs/.local/.django` — `USE_DOCKER`, `REDIS_URL`, `HF_TOKEN`, Flower +- `.envs/.local/.postgres` — `POSTGRES_*` +- `.envs/.production/.django` — `DJANGO_SECRET_KEY`, `DJANGO_ALLOWED_HOSTS`, `SENTRY_DSN`, … + +No container, o entrypoint define `DATABASE_URL` e `CELERY_BROKER_URL` a partir de `POSTGRES_*` e `REDIS_URL`. + +### Variáveis principais + +| Variável | Descrição | +|----------|-----------| +| `DATABASE_URL` | PostgreSQL (montada no entrypoint) | +| `POSTGRES_HOST` / `POSTGRES_PORT` / `POSTGRES_DB` | Credenciais e base | +| `REDIS_URL` | Redis (ex.: `redis://redis:6379/0`) | +| `CELERY_BROKER_URL` | Broker Celery (= `REDIS_URL` no entrypoint) | +| `DJANGO_SECRET_KEY` | Chave secreta (produção) | +| `DJANGO_ALLOWED_HOSTS` | Hosts permitidos | +| `DJANGO_CSRF_TRUSTED_ORIGINS` | Origens CSRF | +| `LLAMA_ENABLED` | Ativar LLM local (`false` em testes) | +| `HF_TOKEN` | Token Hugging Face (download do modelo) | +| `CORE_API_DOMAIN` | API SciELO Core (default `https://core.scielo.org`) | +| `DRF_PAGE_SIZE` | Paginação da API REST | +| `SENTRY_DSN` | Monitorização (produção) | +| `COMPRESS_ENABLED` | Compressor de estáticos (produção) | + +--- + +## Requisitos Python + +| Arquivo | Uso | +|---------|-----| +| `requirements/base.txt` | Runtime | +| `requirements/local.txt` | Dev + pytest | +| `requirements/production.txt` | Produção | + +Após alterar dependências: `make build`. + +--- + +## Comandos úteis + +```bash +make django_shell # shell Django +make django_bash # bash no container +make logs # logs de todos os serviços +make restart # reiniciar containers +make dump_data # backup PostgreSQL +make clean_migrations # limpar migrations (dev) +make clean_project_images # remover imagens Docker do projeto +``` + +**Celery:** serviços `celeryworker` e `celerybeat` no `local.yml`. + +**Tarefas agendadas:** menu _Settings → Tarefas agendadas_ no Wagtail admin. diff --git a/ai/__init__.py b/ai/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ai/apps.py b/ai/apps.py new file mode 100644 index 0000000..c145a50 --- /dev/null +++ b/ai/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class AIConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "ai" + verbose_name = _("AI Model") diff --git a/ai/exceptions.py b/ai/exceptions.py new file mode 100644 index 0000000..c1017de --- /dev/null +++ b/ai/exceptions.py @@ -0,0 +1,8 @@ +class LlamaDisabledError(Exception): + pass + +class LlamaModelNotFoundError(FileNotFoundError): + pass + +class LlamaNotInstalledError(ImportError): + pass diff --git a/ai/extract.py b/ai/extract.py new file mode 100644 index 0000000..c592a74 --- /dev/null +++ b/ai/extract.py @@ -0,0 +1,256 @@ +import logging + +import requests +from django.conf import settings + +from ai.exceptions import ( + LlamaDisabledError, + LlamaModelNotFoundError, + LlamaNotInstalledError, +) +from ai.utils.blocks import source_blocks +from manuscripts.controller import enrich_article +from manuscripts.utils.frontmatter import enrich_frontmatter +from ai.payload import ( + has_useful_payload, + normalize_payload, + payload_counts, +) +from ai.utils.json import try_parse_json +from ai.utils.text import extract_text_from_docx as text_extract_docx +from ai.utils.text import extract_text_from_docx_via_docling +from ai.utils.vision import extract_page_images +from ai.providers.ollama import OllamaProvider +from ai.service import LLMService +from ai.models import GeminiModel, OllamaModel +from ai.prompts.vision import VISION_TASKS +from ai.prompts.text import build_split_task_prompts + +logger = logging.getLogger(__name__) + + +def _detect_provider(): + try: + if GeminiModel.objects.filter(is_active=True).exists(): + return "gemini" + elif OllamaModel.objects.filter(is_active=True).exists(): + return "ollama" + return "llama" + except Exception: + return "unknown" + + +def _fetch_ollama_context(url, model_name): + try: + resp = requests.post(f"{url.rstrip('/')}/api/show", json={"name": model_name}, timeout=10) + if resp.ok: + data = resp.json() + info = data.get("model_info", {}) + for key in info: + if "context_length" in key or "num_ctx" in key: + return info[key] + except Exception: + pass + return None + + +def _effective_limits(provider): + if provider == "gemini": + return 32768, 30000, 60 + if provider == "ollama": + model = OllamaModel.objects.filter(is_active=True).first() + if model: + n_ctx = model.context_limit or _fetch_ollama_context(model.url, model.model) + if n_ctx and n_ctx > 0: + limit_chars = min(int(n_ctx * 3), 200000) + blocks = max(8, min(limit_chars // 500, 60)) + return int(n_ctx), limit_chars, blocks + return 32768, 30000, 15 + + +def _prepare_content(front, body, docx_path, limit_chars, block_limit, use_docling=False): + if docx_path: + try: + extract = extract_text_from_docx_via_docling if use_docling else text_extract_docx + return extract(docx_path, limit_chars), True + except Exception as e: + logger.warning("Failed to extract text from DOCX: %s", e) + + blocks = source_blocks(front, body, limit=block_limit) + if not blocks: + return None, False + return blocks, False + + +def _should_use_vision(docx_path): + if not docx_path: + return False + ollama = OllamaModel.objects.filter(is_active=True).first() + return bool(ollama and ollama.is_vision) + + +def _extract_via_llm_service(article, front, body, docx_path, provider): + n_ctx, limit_chars, block_limit = _effective_limits(provider) + + use_docling = False + if docx_path and provider == "ollama": + ollama = OllamaModel.objects.filter(is_active=True).first() + if ollama: + use_docling = ollama.docx_extractor == OllamaModel.DocxExtractor.DOCLING + + content, is_xml = _prepare_content(front, body, docx_path, limit_chars, block_limit, use_docling) + + if content is None: + return None, {"status": "skipped", "provider": provider, "reason": "no_source_blocks"} + + try: + service = LLMService(mode="prompt", max_tokens=2500, temperature=0.0, top_p=0.1, stop=None, n_ctx=n_ctx) + payload = _run_split_tasks(service, content, is_xml, article.language) + return payload, None + except (LlamaDisabledError, LlamaModelNotFoundError, LlamaNotInstalledError) as exc: + logger.info("LLM unavailable: %s", exc) + return None, {"status": "unavailable", "provider": provider, "error": str(exc)} + except ValueError as exc: + logger.warning("LLM unusable response: %s", exc) + status = "empty_response" if str(exc) == "empty_response" else "invalid_response" + return None, {"status": status, "provider": provider, "error": str(exc)} + except Exception as exc: + logger.exception("LLM extraction failed") + return None, {"status": "failed", "provider": provider, "error": str(exc)} + + +def _make_empty_payload(): + return { + "doi": "", "journal": {"title": "", "issn": ""}, + "issue": {"volume": "", "number": "", "year": "", "supplement": ""}, + "titles": [], "authors": [], "affiliations": [], + "dates": [], "abstracts": [], "keywords": [], "warnings": [], + } + + +def _merge_task_result(payload, name, data): + if name == "titles" and "titles" in data: + payload["titles"] = data["titles"] + elif name in ("authors_affs", "authors"): + if "authors" in data: + payload["authors"] = data["authors"] + if "affiliations" in data: + payload["affiliations"] = data["affiliations"] + elif name == "abstracts" and "abstracts" in data: + payload["abstracts"] = data["abstracts"] + elif name == "keywords" and "keywords" in data: + payload["keywords"] = data["keywords"] + elif name == "dates" and "dates" in data: + payload["dates"] = data["dates"] + elif name == "meta": + for key in ("doi", "journal", "issue"): + if key in data: + payload[key] = data[key] + + +def _run_vision_tasks(provider, images): + payload = _make_empty_payload() + del payload["warnings"] + + for name, prompt in VISION_TASKS: + try: + logger.info("Vision sub-task: %s", name) + response = provider.chat_with_images(images, prompt) + data = try_parse_json(response) + if not data: + logger.warning("Vision sub-task %s: could not parse JSON", name) + continue + + _merge_task_result(payload, name, data) + except Exception as exc: + logger.error("Vision sub-task %s failed: %s", name, exc) + + has_data = any( + payload.get(k) + for k in ("titles", "authors", "affiliations", "abstracts", "keywords", "dates", "doi") + ) + if not has_data: + logger.info("Vision: no data extracted") + return None + + logger.info( + "Vision: titles=%d authors=%d affiliations=%d abstracts=%d keywords=%d dates=%d doi=%s", + len(payload.get("titles") or []), + len(payload.get("authors") or []), + len(payload.get("affiliations") or []), + len(payload.get("abstracts") or []), + len(payload.get("keywords") or []), + len(payload.get("dates") or []), + bool(payload.get("doi")), + ) + return payload + + +def _run_split_tasks(service, content, is_xml, article_language): + payload = _make_empty_payload() + tasks = build_split_task_prompts(content, is_xml, article_language) + + for name, task_prompt, task_format in tasks: + try: + logger.info("Frontmatter sub-task: %s", name) + response_text = service.run(task_prompt, response_format=task_format) + res_json = try_parse_json(response_text) + if not isinstance(res_json, dict): + continue + + _merge_task_result(payload, name, res_json) + + if "warnings" in res_json: + payload["warnings"].extend(res_json["warnings"]) + except Exception as e: + logger.error("Sub-task %s failed: %s", name, e) + payload["warnings"].append(f"Sub-task {name} failed: {e}") + + return payload + + +def extract_frontmatter(article, front, body, docx_path=None): + provider = _detect_provider() + + if not getattr(settings, "MARKUP_DOC_LLM_FRONTMATTER_ENABLED", True): + return front, {}, [], {"status": "disabled", "provider": provider} + + if _should_use_vision(docx_path): + ollama = OllamaModel.objects.filter(is_active=True).first() + + try: + logger.info("Frontmatter: using vision path") + images = extract_page_images(docx_path) + if not images: + logger.info("Frontmatter: no images extracted from DOCX") + return front, {}, [], {"status": "no_data", "provider": provider} + + payload = _run_vision_tasks(OllamaProvider(ollama), images) + if not payload: + return front, {}, [], {"status": "no_data", "provider": provider} + except Exception as exc: + logger.exception("Vision extraction failed") + return front, {}, [{"frontmatter_ai": f"Falha: {exc}"}], {"status": "failed", "provider": provider, "error": str(exc)} + else: + payload, early_return = _extract_via_llm_service(article, front, body, docx_path, provider) + if early_return: + status = early_return["status"] + if status == "unavailable": + msg = "LLM indisponível." + elif status in ("empty_response", "invalid_response"): + msg = f"Resposta inutilizável ({early_return.get('error', status)})." + elif status == "failed": + msg = f"Falha: {early_return.get('error', '')}" + else: + msg = early_return.get("error", "") + return front, {}, [{"frontmatter_ai": msg}] if msg else [], early_return + + normalized, warnings = normalize_payload(payload, article) + + if not has_useful_payload(normalized): + warnings.append("LLM não retornou metadados suficientes.") + return front, {}, [{"frontmatter_ai": w} for w in warnings], {"status": "no_data", "provider": provider, "counts": payload_counts(normalized)} + + enriched_front = enrich_frontmatter(front, normalized, article) + updates = enrich_article(normalized, article) + return enriched_front, updates, [{"frontmatter_ai": w} for w in warnings], {"status": "applied", "provider": provider, "counts": payload_counts(normalized), "updated_fields": sorted(updates)} diff --git a/ai/migrations/0001_initial.py b/ai/migrations/0001_initial.py new file mode 100644 index 0000000..e8aa5f3 --- /dev/null +++ b/ai/migrations/0001_initial.py @@ -0,0 +1,63 @@ +# Generated by Django 6.0.5 on 2026-06-09 00:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='GeminiModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Last update date')), + ('api_key', models.CharField(max_length=255, verbose_name='API Key')), + ('is_active', models.BooleanField(default=False, verbose_name='Active')), + ], + options={ + 'verbose_name': 'Gemini model', + 'verbose_name_plural': 'Gemini models', + }, + ), + migrations.CreateModel( + name='HuggingFaceModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Last update date')), + ('name_model', models.CharField(help_text='e.g. bartowski/Llama-3.2-3B-Instruct-GGUF', max_length=255, verbose_name='Model name')), + ('name_file', models.CharField(help_text='e.g. Llama-3.2-3B-Instruct-Q4_K_M.gguf', max_length=255, verbose_name='Model file')), + ('hf_token', models.CharField(blank=True, max_length=255, verbose_name='HuggingFace token')), + ('download_status', models.IntegerField(blank=True, choices=[(1, 'No model'), (2, 'Downloading'), (3, 'Downloaded'), (4, 'Download error')], default=1, verbose_name='Download status')), + ('is_active', models.BooleanField(default=False, verbose_name='Active')), + ], + options={ + 'verbose_name': 'HuggingFace model', + 'verbose_name_plural': 'HuggingFace models', + }, + ), + migrations.CreateModel( + name='OllamaModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Last update date')), + ('url', models.URLField(help_text='e.g. http://host:11434', verbose_name='Ollama URL')), + ('model', models.CharField(blank=True, help_text='Select after fetching', max_length=255, verbose_name='Model')), + ('is_vision', models.BooleanField(default=False, help_text='Enable to use vision pipeline (page images). When disabled, text pipeline is used.', verbose_name='Vision model')), + ('docx_extractor', models.CharField(choices=[('zipfile', 'Zipfile (built-in)'), ('docling', 'Docling (OCR-capable, requires pytorch)')], default='zipfile', help_text='Method to extract plain text from DOCX for LLM prompts.', max_length=16, verbose_name='DOCX text extractor')), + ('context_limit', models.PositiveIntegerField(blank=True, help_text='Max input tokens for this model. Leave empty for auto-detect via API.', null=True, verbose_name='Context tokens')), + ('is_active', models.BooleanField(default=False, verbose_name='Active')), + ], + options={ + 'verbose_name': 'Ollama model', + 'verbose_name_plural': 'Ollama models', + }, + ), + ] diff --git a/ai/migrations/0002_initial.py b/ai/migrations/0002_initial.py new file mode 100644 index 0000000..7a55b97 --- /dev/null +++ b/ai/migrations/0002_initial.py @@ -0,0 +1,48 @@ +# Generated by Django 6.0.5 on 2026-06-09 00:35 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('ai', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='geminimodel', + name='creator', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_creator', to=settings.AUTH_USER_MODEL, verbose_name='Creator'), + ), + migrations.AddField( + model_name='geminimodel', + name='updated_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_last_mod_user', to=settings.AUTH_USER_MODEL, verbose_name='Updater'), + ), + migrations.AddField( + model_name='huggingfacemodel', + name='creator', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_creator', to=settings.AUTH_USER_MODEL, verbose_name='Creator'), + ), + migrations.AddField( + model_name='huggingfacemodel', + name='updated_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_last_mod_user', to=settings.AUTH_USER_MODEL, verbose_name='Updater'), + ), + migrations.AddField( + model_name='ollamamodel', + name='creator', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_creator', to=settings.AUTH_USER_MODEL, verbose_name='Creator'), + ), + migrations.AddField( + model_name='ollamamodel', + name='updated_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_last_mod_user', to=settings.AUTH_USER_MODEL, verbose_name='Updater'), + ), + ] diff --git a/ai/migrations/__init__.py b/ai/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ai/models.py b/ai/models.py new file mode 100644 index 0000000..00d2262 --- /dev/null +++ b/ai/models.py @@ -0,0 +1,147 @@ +from django import forms +from django.core.exceptions import ValidationError +from django.db import models +from django.utils.translation import gettext_lazy as _ +from wagtail.admin.panels import FieldPanel + +from core.forms import CoreAdminModelForm +from core.models import CommonControlField + + +class MaskedPasswordWidget(forms.PasswordInput): + def __init__(self, attrs=None): + super().__init__(attrs=attrs, render_value=True) + + +class DownloadStatus(models.IntegerChoices): + NO_MODEL = 1, _("No model") + DOWNLOADING = 2, _("Downloading") + DOWNLOADED = 3, _("Downloaded") + ERROR = 4, _("Download error") + + + +class HuggingFaceModel(CommonControlField): + name_model = models.CharField(_("Model name"), max_length=255, help_text="e.g. bartowski/Llama-3.2-3B-Instruct-GGUF") + name_file = models.CharField(_("Model file"), max_length=255, help_text="e.g. Llama-3.2-3B-Instruct-Q4_K_M.gguf") + hf_token = models.CharField(_("HuggingFace token"), max_length=255, blank=True) + download_status = models.IntegerField( + _("Download status"), + choices=DownloadStatus.choices, + default=DownloadStatus.NO_MODEL, + blank=True, + ) + is_active = models.BooleanField(_("Active"), default=False) + + panels = [ + FieldPanel("name_model"), + FieldPanel("name_file"), + FieldPanel("hf_token", widget=MaskedPasswordWidget()), + FieldPanel("download_status", widget=forms.Select(choices=DownloadStatus.choices, attrs={"disabled": True})), + FieldPanel("is_active"), + ] + base_form_class = CoreAdminModelForm + + class Meta: + verbose_name = _("HuggingFace model") + verbose_name_plural = _("HuggingFace models") + + def __str__(self): + return self.name_model or self.name_file or "HuggingFace" + + def clean(self): + if not self.name_model: + raise ValidationError({"name_model": _("Model name is required.")}) + if not self.name_file: + raise ValidationError({"name_file": _("Model file is required.")}) + + def save(self, *args, **kwargs): + if self.is_active: + HuggingFaceModel.objects.exclude(pk=self.pk).update(is_active=False) + super().save(*args, **kwargs) + + + +class OllamaModel(CommonControlField): + url = models.URLField(_("Ollama URL"), help_text="e.g. http://host:11434") + model = models.CharField(_("Model"), max_length=255, blank=True, help_text="Select after fetching") + is_vision = models.BooleanField( + _("Vision model"), + default=False, + help_text="Enable to use vision pipeline (page images). When disabled, text pipeline is used.", + ) + + class DocxExtractor(models.TextChoices): + ZIPFILE = "zipfile", _("Zipfile (built-in)") + DOCLING = "docling", _("Docling (OCR-capable, requires pytorch)") + + docx_extractor = models.CharField( + _("DOCX text extractor"), + max_length=16, + choices=DocxExtractor.choices, + default=DocxExtractor.ZIPFILE, + help_text="Method to extract plain text from DOCX for LLM prompts.", + ) + context_limit = models.PositiveIntegerField( + _("Context tokens"), + blank=True, + null=True, + help_text="Max input tokens for this model. Leave empty for auto-detect via API.", + ) + is_active = models.BooleanField(_("Active"), default=False) + + panels = [ + FieldPanel("url"), + FieldPanel("model", widget=forms.Select(attrs={"data-ai": "ollama-model-select"})), + FieldPanel("is_vision"), + FieldPanel("docx_extractor"), + FieldPanel("context_limit"), + FieldPanel("is_active"), + ] + base_form_class = CoreAdminModelForm + + class Meta: + verbose_name = _("Ollama model") + verbose_name_plural = _("Ollama models") + + def __str__(self): + return f"{self.model or '?'} @ {self.url or '?'}" if self.model else "Ollama" + + def clean(self): + if not self.url: + raise ValidationError({"url": _("Ollama URL is required.")}) + if not self.model: + raise ValidationError({"model": _("Please select a model.")}) + + def save(self, *args, **kwargs): + if self.is_active: + OllamaModel.objects.exclude(pk=self.pk).update(is_active=False) + super().save(*args, **kwargs) + + + +class GeminiModel(CommonControlField): + api_key = models.CharField(_("API Key"), max_length=255) + is_active = models.BooleanField(_("Active"), default=False) + + panels = [ + FieldPanel("api_key", widget=MaskedPasswordWidget()), + FieldPanel("is_active"), + ] + base_form_class = CoreAdminModelForm + + class Meta: + verbose_name = _("Gemini model") + verbose_name_plural = _("Gemini models") + + def __str__(self): + return "Gemini" + + def clean(self): + if not self.api_key: + raise ValidationError({"api_key": _("API Key is required.")}) + + def save(self, *args, **kwargs): + if self.is_active: + GeminiModel.objects.exclude(pk=self.pk).update(is_active=False) + super().save(*args, **kwargs) diff --git a/ai/payload.py b/ai/payload.py new file mode 100644 index 0000000..59707ac --- /dev/null +++ b/ai/payload.py @@ -0,0 +1,184 @@ +import logging +import re + +from ai.utils.normalizers import ( + DOI_RE, + ORCID_RE, + stz_affiliation_id, + stz_country_code, + stz_date, + stz_first_number, + stz_language, + stz_norm, + stz_text, +) + +logger = logging.getLogger(__name__) + + +def payload_counts(payload): + return { + "doi": 1 if payload.get("doi") else 0, + "titles": len(payload.get("titles") or []), + "authors": len(payload.get("authors") or []), + "affiliations": len(payload.get("affiliations") or []), + "dates": len(payload.get("dates") or []), + "abstracts": len(payload.get("abstracts") or []), + "keywords": len(payload.get("keywords") or []), + } + + +def is_invalid_title(text): + clean = stz_norm(text) + heading_words = { + "abstract", "resumo", "resumen", "resumén", "sumário", "sumario", + "keywords", "palavras-chave", "palabras clave", "introduction", + "introdução", "introducción", "methodology", "metodologia", + "conclusion", "conclusão", "conclusión", "references", + "referências", "referencias", "bibliography", "bibliografia", + } + if clean in heading_words: + return True + return ( + len(clean) > 320 + or bool(DOI_RE.search(clean)) + or bool(ORCID_RE.search(clean)) + or "@" in clean + or len(re.findall( + r"\b(universidad|university|instituto|department|facultad|doctor|graduad|research|analysis|study|approach)", + clean, + )) > 2 + ) + + +def normalize_payload(payload, article): + if not isinstance(payload, dict): + return {}, ["LLM response was not a JSON object."] + + warnings = [str(w).strip() for w in payload.get("warnings") or [] if str(w).strip()] + normalized = { + "doi": stz_text(payload.get("doi")), + "titles": [], + "authors": [], + "affiliations": [], + "dates": [], + "abstracts": [], + "keywords": [], + } + + seen_titles = set() + for item in payload.get("titles") or []: + if isinstance(item, str): + text = stz_text(item) + language = stz_language("", article.language) + kind = "main" + else: + text = stz_text(item.get("text")) + language = stz_language(item.get("language"), article.language) + kind = (item.get("kind") or "translated").lower() + if not text or is_invalid_title(text): + if text: + warnings.append(f"Title discarded: {text[:120]}") + continue + key = stz_norm(text) + if key in seen_titles: + continue + seen_titles.add(key) + normalized["titles"].append({ + "text": text, + "language": language, + "kind": kind, + }) + + for index, item in enumerate(payload.get("authors") or [], 1): + if isinstance(item, str): + continue + given_names = stz_text(item.get("given_names")) + surname = stz_text(item.get("surname")) + display = stz_text(item.get("display")) or " ".join( + p for p in [given_names, surname] if p + ) + if not display: + continue + if not given_names and not surname: + parts = display.rsplit(" ", 1) + surname = parts[-1] if len(parts) > 1 else display + given_names = parts[0] if len(parts) > 1 else "" + normalized["authors"].append({ + "given_names": given_names, + "surname": surname, + "display": display, + "orcid": stz_text(item.get("orcid")), + "affiliations": stz_affiliation_id(item.get("affiliations")) or [str(index)], + "symbol": stz_text(item.get("symbol")), + }) + + for index, item in enumerate(payload.get("affiliations") or [], 1): + if isinstance(item, str): + continue + text = stz_text(item.get("text")) + if not text: + continue + normalized["affiliations"].append({ + "id": stz_first_number(item.get("id")) or str(index), + "symbol": stz_text(item.get("symbol")), + "text": text, + "orgname": stz_text(item.get("orgname")), + "orgdiv1": stz_text(item.get("orgdiv1")), + "orgdiv2": stz_text(item.get("orgdiv2")), + "city": stz_text(item.get("city")), + "state": stz_text(item.get("state")), + "country": stz_text(item.get("country")), + "country_code": stz_country_code(item.get("country_code")), + }) + + for item in payload.get("dates") or []: + if isinstance(item, str): + continue + date_type = (item.get("type") or "other").lower() + normalized_date = stz_date(item.get("date") or item.get("raw")) + raw = stz_text(item.get("raw")) or stz_text(item.get("date")) + if date_type not in {"received", "accepted", "published", "ahp", "other"}: + date_type = "other" + if normalized_date or raw: + normalized["dates"].append({ + "type": date_type, "date": normalized_date, "raw": raw, + }) + + for item in payload.get("abstracts") or []: + if isinstance(item, str): + continue + text = stz_text(item.get("text")) + if not text: + continue + normalized["abstracts"].append({ + "title": stz_text(item.get("title")), + "text": text, + "language": stz_language(item.get("language"), article.language), + }) + + for item in payload.get("keywords") or []: + if isinstance(item, str): + continue + terms = [stz_text(t) for t in item.get("terms") or []] + terms = [t for t in terms if t] + if not terms: + continue + normalized["keywords"].append({ + "title": stz_text(item.get("title")), + "terms": terms, + "language": stz_language(item.get("language"), article.language), + }) + + if normalized["titles"] and not any(t["kind"] == "main" for t in normalized["titles"]): + normalized["titles"][0]["kind"] = "main" + normalized["titles"].sort(key=lambda t: 0 if t["kind"] == "main" else 1) + + return normalized, warnings + + +def has_useful_payload(payload): + return any( + payload.get(key) + for key in ("doi", "titles", "authors", "affiliations", "dates", "abstracts", "keywords") + ) diff --git a/ai/prompts/__init__.py b/ai/prompts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ai/prompts/back.py b/ai/prompts/back.py new file mode 100644 index 0000000..0f5dcaf --- /dev/null +++ b/ai/prompts/back.py @@ -0,0 +1,234 @@ +import json + +# Example citation data used for few-shot prompting with LLM-based reference parsing. +# This file is used by the LLM-based reference marker. +# See also ai/prompts/back_gemini.py for the Gemini-specific prompt variant. +MESSAGES = [ + { 'role': 'system', + 'content': 'You are an assistant who distinguishes all the components of a citation in an article with output in JSON' + }, + { 'role': 'user', + 'content': 'Bachman, S., J. Moat, A. W. Hill, J. de la Torre and B. Scott. 2011. Supporting Red List threat assessments with GeoCAT: geospatial conservation assessment tool. ZooKeys 150: 117-126. DOI: https://doi.org/10.3897/zookeys.150.2109' + }, + { 'role': 'assistant', + 'content': json.dumps({ + 'reftype': 'journal', + 'authors': [ + { 'surname': 'Bachman', 'fname': 'S.' }, + { 'surname': 'Moat', 'fname': 'J.' }, + { 'surname': 'Hill', 'fname': 'A. W.' }, + { 'surname': 'de la Torre', 'fname': 'J.' }, + { 'surname': 'Scott', 'fname': 'B.' }, + ], + 'full_text': 'Bachman, S., J. Moat, A. W. Hill, J. de la Torre and B. Scott. 2011. Supporting Red List threat assessments with GeoCAT: geospatial conservation assessment tool. ZooKeys 150: 117-126. DOI: https://doi.org/10.3897/zookeys.150.2109', + 'date': 2011, + 'title': 'Supporting Red List threat assessments with GeoCAT: geospatial conservation assessment tool', + 'source': 'ZooKeys', + 'vol': '150', + 'num': None, + 'fpage': '117', + 'lpage': '126', + 'doi': '10.3897/zookeys.150.2109', + }) + }, + { 'role': 'user', + 'content': 'Brunel, J. F. 1987. Sur le genre Phyllanthus L. et quelques genres voisins de la Tribu des Phyllantheae Dumort. (Euphorbiaceae, Phyllantheae) en Afrique intertropicale et à Madagascar. Thèse de doctorat de l’Université L. Pasteur. Strasbourg, France. 760 pp.' + }, + { 'role': 'assistant', + 'content': json.dumps({ + 'reftype': 'Thesis', + 'authors': [ + { 'surname': 'Brunel', 'fname': 'J. F.' }, + ], + 'full_text': 'Brunel, J. F. 1987. Sur le genre Phyllanthus L. et quelques genres voisins de la Tribu des Phyllantheae Dumort. (Euphorbiaceae, Phyllantheae) en Afrique intertropicale et à Madagascar. Thèse de doctorat de l’Université L. Pasteur. Strasbourg, France. 760 pp.', + 'date': 1987, + 'source': 'Sur le genre Phyllanthus L. et quelques genres voisins de la Tribu des Phyllantheae Dumort. (Euphorbiaceae, Phyllantheae) en Afrique intertropicale et à Madagascar', + 'degree': 'doctorat', + 'organization': 'l’Université L. Pasteur', + 'city': 'Strasbourg', + 'location': 'Strasbourg, France', + 'num_pages': 760 + }), + }, + { + 'role': 'user', + 'content': 'Hernández-López, L. 1995. The endemic flora of Jalisco, Mexico: Centers of endemism and implications for conservation. Tesis de maestría. Universidad de Wisconsin. Madison, USA. 74 pp.' + }, + { 'role': 'assistant', + 'content': json.dumps({ + 'reftype': 'Thesis', + 'authors': [ + { 'surname': 'Hernández-López', 'fname': 'L.' }, + ], + 'full_text': 'Hernández-López, L. 1995. The endemic flora of Jalisco, Mexico: Centers of endemism and implications for conservation. Tesis de maestría. Universidad de Wisconsin. Madison, USA. 74 pp.', + 'date': 1995, + 'source': 'The endemic flora of Jalisco, Mexico: Centers of endemism and implications for conservation', + 'degree': 'maestría', + 'organization': 'Universidad de Wisconsin', + 'location': 'Madison, USA', + 'num_pages': 74 + }), + }, + { + 'role': 'user', + 'content': 'Schimper, A. F. W. 1903. Plant geography upon a physiological basis. Clarendon Press. Oxford, UK. 839 pp.' + }, + { + 'role': 'assistant', + 'content': json.dumps({ + 'reftype': 'book', + 'authors':[ + { 'surname': 'Schimper', 'fname': 'A. F. W.' }, + ], + 'full_text': 'Schimper, A. F. W. 1903. Plant geography upon a physiological basis. Clarendon Press. Oxford, UK. 839 pp.', + 'date': 1903, + 'source': 'Plant geography upon a physiological basis', + 'organization': 'Clarendon Press', + 'location': 'Oxford, UK', + 'num_pages': 839 + }) + }, + { + 'role': 'user', + 'content': 'Correa, M. D., C. Galdames and M. Stapf. 2004. Catálogo de Plantas Vasculares de Panamá. Smithsonian Tropical Research Institute. Ciudad de Panamá, Panamá. 599 pp.' + }, + { + 'role': 'assistant', + 'content': json.dumps({ + 'reftype': 'book', + 'authors':[ + { 'surname': 'Correa', 'fname': 'M. D.' }, + { 'surname': 'Galdames', 'fname': 'C.' }, + { 'surname': 'Stapf', 'fname': 'M.' }, + ], + 'full_text': 'Correa, M. D., C. Galdames and M. Stapf. 2004. Catálogo de Plantas Vasculares de Panamá. Smithsonian Tropical Research Institute. Ciudad de Panamá, Panamá. 599 pp.', + 'date': 2004, + 'source': 'Catálogo de Plantas Vasculares de Panamá', + 'organization': 'Smithsonian Tropical Research Institute', + 'location': 'Ciudad de Panamá, Panamá', + 'num_pages': 599 + }) + }, + { + 'role': 'user', + 'content': 'Hernández-López, L. 2019. Las especies endémicas de plantas en el estado de Jalisco: su distribución y conservación. Comisión Nacional para el Conocimiento y Uso de la Biodiversidad (CONABIO). Cd. Mx., México. https://doi.org/10.15468/ktvqds (consultado diciembre de 2019).' + }, + { + 'role': 'assistant', + 'content': json.dumps({ + 'reftype': 'data', + 'authors':[ + { 'surname': 'Hernández-López', 'fname': 'L.' }, + ], + 'full_text': 'Hernández-López, L. 2019. Las especies endémicas de plantas en el estado de Jalisco: su distribución y conservación. Comisión Nacional para el Conocimiento y Uso de la Biodiversidad (CONABIO). Cd. Mx., México. https://doi.org/10.15468/ktvqds (consultado diciembre de 2019).', + 'date': 2019, + 'title': 'Las especies endémicas de plantas en el estado de Jalisco: su distribución y conservación', + 'source': 'Comisión Nacional para el Conocimiento y Uso de la Biodiversidad (CONABIO)', + 'location': 'Cd. Mx. México', + 'doi': '10.15468/ktvqds', + 'access_date': 'diciembre de 2019' + }) + }, + { + 'role': 'user', + 'content': 'INAFED. 2010. Enciclopedia de los Municipios y Delegaciones de México: Jalisco. Instituto Nacional para el Federalismo y el Desarrollo Municipal. http://www.inafed.gob.mx/ work/enciclopedia/EMM21puebla/index.html (consultado diciembre de 2018).' + }, + { + 'role': 'assistant', + 'content': json.dumps({ + 'reftype': 'webpage', + 'authors':[ + { 'collab': 'INAFED' }, + ], + 'full_text': 'INAFED. 2010. Enciclopedia de los Municipios y Delegaciones de México: Jalisco. Instituto Nacional para el Federalismo y el Desarrollo Municipal. http://www.inafed.gob.mx/ work/enciclopedia/EMM21puebla/index.html (consultado diciembre de 2018).', + 'date': 2010, + 'source': 'Enciclopedia de los Municipios y Delegaciones de México: Jalisco', + 'organization': 'Instituto Nacional para el Federalismo y el Desarrollo Municipal', + 'uri': 'http://www.inafed.gob.mx/ work/enciclopedia/EMM21puebla/index.html', + 'access_date': 'diciembre de 2018' + }) + }, + { + 'role': 'user', + 'content': 'Nikon Corporation. 1991-2006. NIS- Elements, version 2.33. Tokio, Japón.' + }, + { + 'role': 'assistant', + 'content': json.dumps({ + 'reftype': 'software', + 'authors':[ + { 'collab': 'Nikon Corporation' }, + ], + 'full_text': 'Nikon Corporation. 1991-2006. NIS- Elements, version 2.33. Tokio, Japón.', + 'date': 2006, + 'source': 'NIS- Elements', + 'version': '2.33', + 'city': 'Tokio', + 'country': 'Japón', + }) + }, + { + 'role': 'user', + 'content': 'Furton EJ, Dort V, editors. Addiction and compulsive behaviors. Proceedings of the 17th Workshop for Bishops; 1999; Dallas, TX. Boston: National Catholic Bioethics Center (US); 2000. 258 p.' + }, + { + 'role': 'assistant', + 'content': json.dumps({ + 'reftype': 'confproc', + 'full_text': 'Furton EJ, Dort V, editors. Addiction and compulsive behaviors. Proceedings of the 17th Workshop for Bishops; 1999; Dallas, TX. Boston: National Catholic Bioethics Center (US); 2000. 258 p.', + 'authors':[ + { 'surname': 'Furton', 'fname': 'EJ' }, + { 'surname': 'Dort', 'fname': 'V' }, + ], + 'date': 2000, + 'source': 'Addiction and compulsive behaviors', + 'title': 'Proceedings of the 17th Workshop for Bishops', + 'location': 'Dallas, TX', + 'num': '17', + 'organization': 'National Catholic Bioethics Center (US)', + 'org_location': 'Boston', + 'pages': '258 p' + }) + }, + ] + +RESPONSE_FORMAT = { + 'type': 'json_object', + 'schema':{ + 'type': 'object', + 'properties': { + 'reftype': {'type': 'string', 'enum': ['journal', 'thesis', 'book', 'data', 'webpage', 'software', 'confproc']}, + 'authors': {'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'surname': {'type': 'string'}, + 'fname': {'type': 'string'}, + 'collab': {'type': 'string'} + } + } + }, + "full_text": {"type": "string"}, + "date": {"type": "integer"}, + "title": {"type": "string"}, + "chapter": {"type": "string"}, + "edition": {"type": "string"}, + "source": {"type": "string"}, + "vol": {"type": "integer"}, + "num": {"type": "integer"}, + "pages": {"type": "string"}, + "fpage": {"type": "string"}, + "lpage": {"type": "string"}, + "doi": {"type": "string"}, + "degree": {"type": "string"}, + "organization": {"type": "string"}, + "location": {"type": "string"}, + "org_location": {"type": "string"}, + "num_pages": {"type": "integer"}, + "uri": {"type": "string"}, + "access_id": {"type": "string"}, + "access_date": {"type": "string"}, + "version": {"type": "string"}, + }, + } + } \ No newline at end of file diff --git a/ai/prompts/text.py b/ai/prompts/text.py new file mode 100644 index 0000000..67d2e3f --- /dev/null +++ b/ai/prompts/text.py @@ -0,0 +1,258 @@ +import json + +JSON_SCHEMAS = { + "titles": { + "type": "object", + "properties": { + "titles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "text": {"type": "string"}, + "language": {"type": "string"}, + "kind": {"type": "string", "enum": ["main", "translated"]}, + }, + "required": ["text", "language", "kind"], + }, + }, + }, + "required": ["titles"], + }, + "authors_affs": { + "type": "object", + "properties": { + "authors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "given_names": {"type": "string"}, + "surname": {"type": "string"}, + "orcid": {"type": "string"}, + "affiliations": {"type": "array", "items": {"type": "string"}}, + "symbol": {"type": "string"}, + "display": {"type": "string"}, + }, + "required": ["given_names", "surname", "orcid", "affiliations", "display"], + }, + }, + "affiliations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "symbol": {"type": "string"}, + "text": {"type": "string"}, + "orgname": {"type": "string"}, + "orgdiv1": {"type": "string"}, + "orgdiv2": {"type": "string"}, + "city": {"type": "string"}, + "state": {"type": "string"}, + "country": {"type": "string"}, + "country_code": {"type": "string"}, + }, + "required": ["id", "text"], + }, + }, + }, + }, + "abstracts": { + "type": "object", + "properties": { + "abstracts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": {"type": "string"}, + "text": {"type": "string"}, + "language": {"type": "string"}, + }, + "required": ["title", "text", "language"], + }, + }, + }, + }, + "keywords": { + "type": "object", + "properties": { + "keywords": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": {"type": "string"}, + "terms": {"type": "array", "items": {"type": "string"}}, + "language": {"type": "string"}, + }, + "required": ["title", "terms", "language"], + }, + }, + }, + }, + "dates": { + "type": "object", + "properties": { + "dates": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": {"type": "string", "enum": ["received", "accepted", "published", "ahp", "other"]}, + "date": {"type": "string"}, + "raw": {"type": "string"}, + }, + "required": ["type", "date", "raw"], + }, + }, + }, + }, + "meta": { + "type": "object", + "properties": { + "doi": {"type": "string"}, + "journal": { + "type": "object", + "properties": { + "title": {"type": "string"}, + "issn": {"type": "string"}, + }, + "required": ["title", "issn"], + }, + "issue": { + "type": "object", + "properties": { + "volume": {"type": "string"}, + "number": {"type": "string"}, + "year": {"type": "string"}, + "supplement": {"type": "string"}, + }, + "required": ["volume", "number", "year", "supplement"], + }, + }, + }, +} + + +def build_title_prompt(content_str, article_language): + return ( + f"Extract the article title and any translated titles from this academic paper. " + f"The title is the main heading at the top. Section headings such as Abstract, " + f"Introduction, Methodology, Results, Discussion, Conclusion, and References are NOT " + f"titles — skip them. There must be exactly one main title. " + f"The article language is {article_language}. " + f'Return a JSON object with a "titles" key containing a list of objects, each with ' + f'"text" (the title string), "language" (ISO 639-1 code: en, es, pt, fr), and ' + f'"kind" ("main" for the primary title, "translated" for translations). ' + f'If no title is found, return an empty titles array. ' + f'Example: {{"titles":[{{"text":"The Main Title","language":"en","kind":"main"}},' + f'{{"text":"El Título","language":"es","kind":"translated"}}]}}. ' + f"Content:\n{content_str}" + ) + + +def build_auth_prompt(content_str): + return ( + "Extract all authors and their affiliations from this academic paper. " + "For each author, collect given names, surname (compound surnames like " + "\"de la Torre\" or \"da Silva\" go entirely in surname), ORCID (look for " + "0000-0000-0000-0000 pattern near the name; use empty string if not found), " + "affiliation IDs (e.g. [\"1\"] or [\"1\",\"3\"] for multiple), the affiliation " + "symbol next to the name (*, **, \u2020, etc.), and a display name with the " + "full author name text. Never invent author names or ORCIDs — only extract " + "what is in the content. " + "For each affiliation, extract the id (e.g. \"1\"), the symbol, the full original " + "text, institution name (orgname), division/department (orgdiv1), sub-division " + "(orgdiv2), city, state, country, and country code (ISO alpha-2 uppercase: BR, US, ES). " + "If no authors are found, return empty arrays for both authors and affiliations. " + 'Return a JSON object with "authors" and "affiliations" keys. ' + 'Example: {"authors":[{"given_names":"John","surname":"Smith","orcid":"0000-0000-0000-0000",' + '"affiliations":["1"],"symbol":"*","display":"John Smith"}],' + '"affiliations":[{"id":"1","symbol":"*","text":"University of Example, City, Country",' + '"orgname":"University of Example","orgdiv1":"","orgdiv2":"","city":"City","state":"",' + '"country":"Country","country_code":"US"}]}. ' + f"Content:\n{content_str}" + ) + + +def build_abstracts_prompt(content_str): + return ( + "Extract all abstracts from this academic paper. Copy the exact original text " + "verbatim — do not summarize, truncate, paraphrase, or translate. " + "Join multi-paragraph abstracts with a single space. " + "Detect the language from the heading: Resumen\u2192es, Resumo\u2192pt, Abstract\u2192en, " + "R\u00e9sum\u00e9\u2192fr. If no heading label is present, infer the language from " + "the text itself. Do NOT include keyword sections — only the abstract body. " + 'Return a JSON object with an "abstracts" key containing a list of objects, each ' + 'with "title" (the exact label: "Abstract", "Resumo", "Resumen", etc.), ' + '"text" (the abstract body), and "language" (ISO 639-1 code). ' + 'Example: {"abstracts":[{"title":"Abstract","text":"The full abstract text...","language":"en"}]}. ' + f"Content:\n{content_str}" + ) + + +def build_keywords_prompt(content_str): + return ( + "Extract all keyword groups from this academic paper. Split terms on semicolons, " + "commas, bullets (\u00b7 \u2022), hyphens, tabs, or newlines. " + "Multi-word phrases are single terms (e.g. \"climate change\", " + "\"inteligencia artificial\") — do not split on spaces within a concept. " + "Deduplicate case-insensitively, keeping the first occurrence's casing. " + 'Return a JSON object with a "keywords" key containing a list of objects, each ' + 'with "title" (the label: "Keywords", "Palavras-chave", "Palabras clave"), ' + '"terms" (list of keyword strings), and "language" (ISO 639-1 code). ' + 'Example: {"keywords":[{"title":"Keywords","terms":["climate change","ecology"],"language":"en"}]}. ' + f"Content:\n{content_str}" + ) + + +def build_dates_prompt(content_str): + return ( + "Extract all dates from this academic paper. Normalize to YYYY-MM-DD format " + "(or YYYY-MM, YYYY for partial dates). " + "Recognize month names in English, Spanish, Portuguese, and French " + "(January/enero/janeiro/janvier, etc.) and their abbreviations (Jan/Ene/Jan/F\u00e9v). " + "Latin American articles typically use DD-MM-YYYY format — convert appropriately. " + "Types: received, accepted, published, ahp (ahead of print/Epub/online first), other. " + "The \"raw\" field must contain the original source text exactly as found. " + 'Return a JSON object with a "dates" key containing a list of objects, each with ' + '"type", "date" (YYYY-MM-DD), and "raw". ' + 'Example: {"dates":[{"type":"received","date":"2023-01-15","raw":"Received 15 January 2023"},' + '{"type":"accepted","date":"2023-06-20","raw":"Accepted: 20/06/2023"}]}. ' + f"Content:\n{content_str}" + ) + + +def build_meta_prompt(content_str): + return ( + "Extract DOI, journal ISSN, and issue metadata from this academic paper. " + "Look for the DOI pattern 10.XXXX/... near the top, often after \"DOI:\" or " + "\"https://doi.org/\". For ISSN, look for \"ISSN:\", \"ISSN-L:\", \"eISSN:\", " + "\"pISSN:\" or the 8-digit pattern (1234-5678). For the issue, look for " + "patterns like \"Vol. 15, No. 3\", \"v.15, n.3\". Supplements appear as " + "\"Suppl 1\", \"Suplemento 2\", \"S1\". Extract the publication year, not the " + "copyright year. Use empty strings for any field not found. " + 'Return a JSON object with "doi", "journal" (with "title" and "issn"), and ' + '"issue" (with "volume", "number", "year", "supplement"). ' + 'Example: {"doi":"10.1234/example.2023","journal":{"title":"Journal of Examples","issn":"1234-5678"},' + '"issue":{"volume":"15","number":"3","year":"2023","supplement":""}}. ' + f"Content:\n{content_str}" + ) + + +def build_split_task_prompts(content, is_xml, article_language): + if is_xml: + content_str = content + else: + content_str = json.dumps(content, ensure_ascii=False, indent=2) + + return [ + ("titles", build_title_prompt(content_str, article_language), JSON_SCHEMAS["titles"]), + ("authors_affs", build_auth_prompt(content_str), JSON_SCHEMAS["authors_affs"]), + ("abstracts", build_abstracts_prompt(content_str), JSON_SCHEMAS["abstracts"]), + ("keywords", build_keywords_prompt(content_str), JSON_SCHEMAS["keywords"]), + ("dates", build_dates_prompt(content_str), JSON_SCHEMAS["dates"]), + ("meta", build_meta_prompt(content_str), JSON_SCHEMAS["meta"]), + ] diff --git a/ai/prompts/vision.py b/ai/prompts/vision.py new file mode 100644 index 0000000..31b3f97 --- /dev/null +++ b/ai/prompts/vision.py @@ -0,0 +1,56 @@ +VISION_TASKS = [ + ("titles", + "Analyze the pages of this article and extract the main title and any translated titles, " + "with their languages. Return a JSON object with a \"titles\" key containing a list of objects " + "with \"text\", \"language\" (ISO 639-1: en, es, pt, fr), and \"kind\" (\"main\" or \"translated\"). " + "Example: {\"titles\":[{\"text\":\"The Title\",\"language\":\"en\",\"kind\":\"main\"}," + "{\"text\":\"El T\u00edtulo\",\"language\":\"es\",\"kind\":\"translated\"}]}. " + "Do not include explanations, only the JSON.", + ), + ("authors", + "Analyze the pages of this article and extract all author names, their ORCIDs " + "(format 0000-0000-0000-0000), and affiliation symbols (*, **, etc). " + "Return a JSON object with an \"authors\" key containing a list of objects with \"given_names\", " + "\"surname\", \"display\", \"orcid\", and \"affiliations\" (list of IDs). " + "Use empty string \"\" for ORCID if not found. NEVER invent ORCIDs. " + "Also return \"affiliations\", a list of objects with \"id\" and \"text\". " + "Example: {\"authors\":[{\"given_names\":\"John\",\"surname\":\"Smith\"," + "\"display\":\"John Smith\",\"orcid\":\"0000-0000-0000-0000\",\"affiliations\":[\"1\"]}]," + "\"affiliations\":[{\"id\":\"1\",\"text\":\"University of Example\"}]}. " + "Do not include explanations, only the JSON.", + ), + ("abstracts", + "Analyze the pages of this article and extract all abstracts " + "(Abstract, Resumo, Resumen, R\u00e9sum\u00e9, etc.), with their respective languages. " + "Return a JSON object with an \"abstracts\" key containing a list of objects with " + "\"title\" (exact label: \"Abstract\", \"Resumo\", etc.), " + "\"text\" (only the abstract body text, do NOT include keywords), " + "and \"language\" (ISO 639-1). " + "Example: {\"abstracts\":[{\"title\":\"Abstract\",\"text\":\"The full abstract text...\",\"language\":\"en\"}]}. " + "Do not include explanations, only the JSON.", + ), + ("keywords", + "Analyze the pages of this article and extract all keyword groups, " + "with their respective languages. Return a JSON object with a \"keywords\" key containing a " + "list of objects with \"title\" (label: \"Keywords\", \"Palavras-chave\", etc.), " + "\"terms\" (list of terms), and \"language\" (ISO 639-1). " + "Example: {\"keywords\":[{\"title\":\"Keywords\",\"terms\":[\"term1\",\"term2\"],\"language\":\"en\"}]}. " + "Do not include explanations, only the JSON.", + ), + ("dates", + "Analyze the pages of this article and extract the received, accepted, and publication dates. " + "Return a JSON object with a \"dates\" key containing a list of objects with \"type\" " + "(\"received\", \"accepted\", \"published\", \"ahp\"), \"date\" (format YYYY-MM-DD), " + "and \"raw\" (original date text). " + "Example: {\"dates\":[{\"type\":\"received\",\"date\":\"2023-01-15\",\"raw\":\"Received: 15.01.2023\"}]}. " + "Do not include explanations, only the JSON.", + ), + ("meta", + "Analyze the pages of this article and extract the DOI, journal name, ISSN, " + "volume, number, and year. Return a JSON object with keys \"doi\", \"journal\" (with \"title\" and " + "\"issn\"), and \"issue\" (with \"volume\", \"number\", \"year\"). " + "Example: {\"doi\":\"10.1234/example.2023\",\"journal\":{\"title\":\"Journal Name\",\"issn\":\"1234-5678\"}," + "\"issue\":{\"volume\":\"15\",\"number\":\"3\",\"year\":\"2023\"}}. " + "Do not include explanations, only the JSON.", + ), +] diff --git a/ai/providers/__init__.py b/ai/providers/__init__.py new file mode 100644 index 0000000..26c0ece --- /dev/null +++ b/ai/providers/__init__.py @@ -0,0 +1,8 @@ +JSON_INSTRUCTION = ( + "Output ONLY a JSON object, never text before or after. " + "Start with {, end with }. " + "CRITICAL: never invent names, ORCIDs, dates, or any data. " + "Use empty string \"\" for missing fields. " + "Use empty array [] for missing lists. " + "Only extract what is explicitly written in the content." +) diff --git a/ai/providers/gemini.py b/ai/providers/gemini.py new file mode 100644 index 0000000..201bf92 --- /dev/null +++ b/ai/providers/gemini.py @@ -0,0 +1,54 @@ +import logging +import time + +import google.generativeai as genai + +GEMINI_MODEL = "models/gemini-3.1-flash-lite-preview" +logger = logging.getLogger(__name__) + + +class GeminiProvider: + def __init__(self, model, response_format=None): + genai.configure(api_key=model.api_key) + self.model = model + self.response_format = response_format + + def chat(self, messages): + started = time.monotonic() + prompt_parts = [] + for msg in messages: + role = msg.get("role", "") + content = msg.get("content", "") + if role == "system": + prompt_parts.append(f"System instruction:\n{content}\n") + elif role == "user": + prompt_parts.append(f"User: {content}") + elif role == "assistant": + prompt_parts.append(f"Assistant: {content}") + prompt = "\n".join(prompt_parts) + + generation_config = {} + if self.response_format and self.response_format.get("type") == "json_object": + generation_config["response_mime_type"] = "application/json" + + model = genai.GenerativeModel(GEMINI_MODEL) + response_text = model.generate_content(prompt, generation_config=generation_config).text + elapsed = time.monotonic() - started + logger.info("Gemini chat: %d chars in %.2fs", len(response_text or ""), elapsed) + time.sleep(15) + return {"choices": [{"message": {"content": response_text}}]} + + def prompt(self, user_input, response_format=None): + started = time.monotonic() + model = genai.GenerativeModel(GEMINI_MODEL) + generation_config = {"response_mime_type": "application/json"} + if response_format: + generation_config["response_schema"] = response_format + response_text = model.generate_content( + user_input, + generation_config=generation_config, + ).text + elapsed = time.monotonic() - started + logger.info("Gemini prompt: %d chars in %.2fs", len(response_text or ""), elapsed) + time.sleep(15) + return response_text diff --git a/ai/providers/local.py b/ai/providers/local.py new file mode 100644 index 0000000..8fee3ef --- /dev/null +++ b/ai/providers/local.py @@ -0,0 +1,86 @@ +import logging +import os +import time + +from ai.exceptions import ( + LlamaDisabledError, + LlamaModelNotFoundError, + LlamaNotInstalledError, +) +from ai.providers import JSON_INSTRUCTION +from config.settings.base import LLAMA_ENABLED, LLAMA_MODEL_DIR + +logger = logging.getLogger(__name__) + + +class LocalProvider: + _cached_llm = None + + def __init__(self, model, response_format=None, temperature=0.0, top_p=0.1, max_tokens=4000, stop=None, n_ctx=None, nthreads=2): + self.model = model + self.response_format = response_format + self.temperature = temperature + self.top_p = top_p + self.max_tokens = max_tokens + self.stop = stop + self.n_ctx = n_ctx or 32768 + + if not LLAMA_ENABLED: + raise LlamaDisabledError("LLaMA is disabled.") + + if LocalProvider._cached_llm is None: + try: + from llama_cpp import Llama + except ImportError as e: + raise LlamaNotInstalledError("llama-cpp-python not installed.") from e + + model_path = os.path.join(LLAMA_MODEL_DIR, self.model.name_file) + if not os.path.isfile(model_path): + raise LlamaModelNotFoundError(f"Model file not found at {model_path}.") + + logger.info("Loading local Llama: %s", model_path) + LocalProvider._cached_llm = Llama( + model_path=model_path, n_ctx=self.n_ctx, n_threads=nthreads + ) + logger.info("Local Llama loaded.") + + self.llm = LocalProvider._cached_llm + + def chat(self, messages): + started = time.monotonic() + logger.info("Local Llama chat. Preview: %r", messages[-1]["content"][:150]) + response = self.llm.create_chat_completion( + messages=messages, + response_format=self.response_format, + max_tokens=self.max_tokens, + temperature=self.temperature, + top_p=self.top_p, + ) + elapsed = time.monotonic() - started + try: + response_text = response["choices"][0]["message"]["content"] + except (KeyError, IndexError, TypeError): + response_text = "" + logger.info("Local Llama chat: %d chars in %.2fs", len(response_text), elapsed) + return response + + def prompt(self, user_input, response_format=None): + started = time.monotonic() + logger.info("Local Llama prompt. Preview: %r", user_input[:150]) + messages = [ + {"role": "system", "content": JSON_INSTRUCTION}, + {"role": "user", "content": user_input}, + ] + response = self.llm.create_chat_completion( + messages=messages, + max_tokens=self.max_tokens, + temperature=self.temperature, + stop=self.stop, + ) + elapsed = time.monotonic() - started + try: + response_text = response["choices"][0]["message"]["content"] + except (KeyError, IndexError, TypeError): + response_text = "" + logger.info("Local Llama prompt: %d chars in %.2fs", len(response_text), elapsed) + return response_text diff --git a/ai/providers/ollama.py b/ai/providers/ollama.py new file mode 100644 index 0000000..694e4f4 --- /dev/null +++ b/ai/providers/ollama.py @@ -0,0 +1,121 @@ +import json +import logging +import time + +import requests + +from ai.providers import JSON_INSTRUCTION + +logger = logging.getLogger(__name__) + + +class OllamaProvider: + def __init__(self, model, response_format=None, temperature=0.0, top_p=0.1, max_tokens=4000, stop=None): + self.model = model + self.response_format = response_format + self.temperature = temperature + self.top_p = top_p + self.max_tokens = max_tokens + self.stop = stop + + def _url(self, path="api/chat"): + return f"{self.model.url.rstrip('/')}/{path}" + + def chat(self, messages): + started = time.monotonic() + logger.info("Ollama chat. Preview: %r", messages[-1]["content"][:150]) + + options = {"temperature": self.temperature, "top_p": self.top_p} + if self.max_tokens: + options["num_predict"] = self.max_tokens + + payload = { + "model": self.model.model, + "messages": messages, + "options": options, + "stream": False, + } + if self.response_format and self.response_format.get("type") == "json_object": + payload["format"] = "json" + + try: + resp = requests.post(self._url(), json=payload, timeout=300) + resp.raise_for_status() + response_text = resp.json()["message"]["content"] + except Exception as exc: + logger.error("Ollama chat error: %s", exc) + response_text = "" + + elapsed = time.monotonic() - started + logger.info("Ollama chat: %d chars in %.2fs", len(response_text), elapsed) + return {"choices": [{"message": {"content": response_text}}]} + + def prompt(self, user_input, response_format=None): + started = time.monotonic() + logger.info("Ollama prompt. Preview: %r", user_input[:150]) + + options = {"temperature": self.temperature, "enable_thinking": False} + if self.max_tokens: + options["num_predict"] = self.max_tokens + if self.stop: + options["stop"] = self.stop + + payload = { + "model": self.model.model, + "messages": [ + {"role": "system", "content": JSON_INSTRUCTION}, + {"role": "user", "content": user_input}, + ], + "stream": False, + "options": options, + } + if response_format: + payload["format"] = response_format + elif self.response_format and self.response_format.get("type") == "json_object": + payload["format"] = "json" + + try: + resp = requests.post(self._url(), json=payload, timeout=300) + resp.raise_for_status() + response_text = resp.json().get("message", {}).get("content") or "" + elapsed = time.monotonic() - started + logger.info("Ollama prompt: %d chars in %.2fs", len(response_text), elapsed) + return response_text + except Exception as exc: + logger.error("Ollama prompt error: %s", exc) + return "" + + def chat_with_images(self, images, prompt, model=None): + started = time.monotonic() + model_name = model or self.model.model + logger.info("Ollama vision. Model=%s images=%d prompt=%r", model_name, len(images), prompt[:150]) + + payload = { + "model": model_name, + "messages": [{"role": "user", "content": prompt, "images": images}], + "stream": True, + "options": {"temperature": 0.0, "num_ctx": 16384, "num_predict": 16384}, + } + try: + resp = requests.post(self._url(), json=payload, timeout=300) + resp.raise_for_status() + except Exception as exc: + logger.error("Ollama vision error: %s", exc) + return "" + + parts = [] + for line in resp.iter_lines(decode_unicode=True): + if not line: + continue + try: + chunk = json.loads(line) + c = chunk.get("message", {}).get("content", "") + if c: + parts.append(c) + except json.JSONDecodeError: + continue + + response_text = "".join(parts) + elapsed = time.monotonic() - started + logger.info("Ollama vision: %d chars in %.2fs", len(response_text), elapsed) + return response_text diff --git a/ai/references.py b/ai/references.py new file mode 100644 index 0000000..ffe58c7 --- /dev/null +++ b/ai/references.py @@ -0,0 +1,41 @@ +import logging + +from ai.exceptions import ( + LlamaDisabledError, + LlamaModelNotFoundError, + LlamaNotInstalledError, +) +from ai.service import LLMService +from ai.prompts.back import MESSAGES, RESPONSE_FORMAT + +logger = logging.getLogger(__name__) + + +def mark_reference(reference_text): + try: + reference_marker = LLMService(MESSAGES, RESPONSE_FORMAT) + output = reference_marker.run(reference_text) + for item in output.get("choices", []): + yield item.get("message", {}).get("content", "") + + except (LlamaDisabledError, LlamaNotInstalledError, LlamaModelNotFoundError) as e: + logger.error("Error marking reference: %s — ref=%s", e, reference_text) + if isinstance(e, LlamaModelNotFoundError): + yield f"Llama model file not found: {str(e)}" + else: + yield f"Llama model is not available: {str(e)}" + + except Exception as e: + logger.exception("Unexpected error marking reference: ref=%s", reference_text) + yield f"An unexpected error occurred: {str(e)}" + + +def mark_references(reference_block): + for ref_row in reference_block.split("\n"): + ref_row = ref_row.strip() + if ref_row: + choices = mark_reference(ref_row) + yield { + "references": ref_row, + "choices": list(choices) + } diff --git a/ai/service.py b/ai/service.py new file mode 100644 index 0000000..79f8982 --- /dev/null +++ b/ai/service.py @@ -0,0 +1,63 @@ +import logging + +from ai.exceptions import LlamaModelNotFoundError +from ai.models import GeminiModel, HuggingFaceModel, OllamaModel +from ai.providers.gemini import GeminiProvider +from ai.providers.local import LocalProvider +from ai.providers.ollama import OllamaProvider + +logger = logging.getLogger(__name__) + + +class LLMService: + def __init__( + self, + messages=None, + response_format=None, + max_tokens=4000, + temperature=0.0, + top_p=0.1, + mode="chat", + nthreads=2, + stop=None, + n_ctx=None, + ): + self.messages = messages + self.response_format = response_format + self.mode = mode + + provider_kwargs = { + "response_format": response_format, + "temperature": temperature, + "top_p": top_p, + "max_tokens": max_tokens, + "stop": stop, + } + + gemini = GeminiModel.objects.filter(is_active=True).first() + if gemini: + logger.info("LLMService: using Gemini") + self.provider = GeminiProvider(gemini, response_format=response_format) + return + + ollama = OllamaModel.objects.filter(is_active=True).first() + if ollama: + logger.info("LLMService: using Ollama at %s", ollama.url) + self.provider = OllamaProvider(ollama, **provider_kwargs) + return + + hf = HuggingFaceModel.objects.filter(is_active=True).first() + if hf: + logger.info("LLMService: using local Llama") + self.provider = LocalProvider(hf, n_ctx=n_ctx, nthreads=nthreads, **provider_kwargs) + return + + raise LlamaModelNotFoundError("No AI model configured.") + + def run(self, user_input, response_format=None): + if self.mode == "chat": + messages = self.messages.copy() + messages.append({"role": "user", "content": user_input}) + return self.provider.chat(messages) + elif self.mode == "prompt": + return self.provider.prompt(user_input, response_format or self.response_format) diff --git a/ai/tasks.py b/ai/tasks.py new file mode 100644 index 0000000..43698e8 --- /dev/null +++ b/ai/tasks.py @@ -0,0 +1,52 @@ +import logging +import os + +from huggingface_hub import hf_hub_download, login + +from ai.models import DownloadStatus, HuggingFaceModel +from config import celery_app +from config.settings.base import LLAMA_MODEL_DIR + +logger = logging.getLogger(__name__) + + +def _download_hf_model(hf_token, model_name, model_file): + if hf_token: + login(token=hf_token) + hf_hub_download(repo_id=model_name, filename=model_file, local_dir=LLAMA_MODEL_DIR) + + +@celery_app.task() +def download_model(instance_id=None): + logger.info("Download task started. instance_id=%s", instance_id) + try: + if instance_id is None: + instance = HuggingFaceModel.objects.first() + if not instance: + logger.info("No HuggingFace model found. Creating default...") + hf_token = os.getenv("HF_TOKEN", "") + instance = HuggingFaceModel.objects.create( + name_model="hugging-quants/Llama-3.2-3B-Instruct-Q4_K_M-GGUF", + name_file="llama-3.2-3b-instruct-q4_k_m.gguf", + hf_token=hf_token, + download_status=DownloadStatus.DOWNLOADING, + ) + else: + instance.download_status = DownloadStatus.DOWNLOADING + instance.save() + else: + instance = HuggingFaceModel.objects.get(pk=instance_id) + instance.download_status = DownloadStatus.DOWNLOADING + instance.save() + + logger.info("Downloading %s / %s...", instance.name_model, instance.name_file) + _download_hf_model(instance.hf_token, instance.name_model, instance.name_file) + instance.download_status = DownloadStatus.DOWNLOADED + instance.save() + logger.info("Download complete.") + except Exception as e: + logger.error("Download failed: %s", e, exc_info=True) + instance = locals().get("instance") + if instance: + instance.download_status = DownloadStatus.ERROR + instance.save() diff --git a/ai/templates/wagtailadmin/icons/ai-brain.svg b/ai/templates/wagtailadmin/icons/ai-brain.svg new file mode 100644 index 0000000..bacd445 --- /dev/null +++ b/ai/templates/wagtailadmin/icons/ai-brain.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/ai/utils/__init__.py b/ai/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ai/utils/blocks.py b/ai/utils/blocks.py new file mode 100644 index 0000000..0e54678 --- /dev/null +++ b/ai/utils/blocks.py @@ -0,0 +1,45 @@ +from ai.utils.normalizers import stz_language +from labeling.fragments import plain_paragraph_text + + +def block_parts(item): + if hasattr(item, "block_type") and hasattr(item, "value"): + return item.block_type, dict(item.value) + if isinstance(item, dict): + return item.get("type", ""), item.get("value") or {} + return "", {} + + +def block_text(value): + text = value.get("paragraph") or value.get("text_aff") or value.get("original") or value.get("title") or "" + return plain_paragraph_text(str(text)).strip() + + +def make_block(label, text): + return {"type": "paragraph", "value": {"label": label, "paragraph": text or ""}} + + +def make_lang_block(label, text, language): + return { + "type": "paragraph_with_language", + "value": {"label": label, "language": stz_language(language, "en"), "paragraph": text or ""}, + } + + +def source_blocks(front, body, limit=60): + blocks = [] + for section, items in (("front", front), ("body", body[:25] if isinstance(body, list) else body)): + for index, item in enumerate(items or []): + block_type, value = block_parts(item) + text = block_text(value) + if not text: + continue + max_text = 6000 if limit >= 60 else 2000 + blocks.append({ + "section": section, "index": index, + "type": block_type, "label": value.get("label", ""), + "text": text[:max_text], + }) + if len(blocks) >= limit: + return blocks + return blocks diff --git a/ai/utils/json.py b/ai/utils/json.py new file mode 100644 index 0000000..b67d5d1 --- /dev/null +++ b/ai/utils/json.py @@ -0,0 +1,80 @@ +import json +import re + + +def fix_unicode_escapes(text): + result = [] + i = 0 + while i < len(text): + if text[i:i+2] == "\\u": + after = text[i+2:i+6] + if len(after) == 4 and all(c in "0123456789abcdefABCDEF" for c in after): + result.append(text[i:i+6]) + i += 6 + continue + result.append("\\\\u") + i += 2 + continue + result.append(text[i]) + i += 1 + return "".join(result) + + +def extract_balanced_json(text): + start = text.find("{") + while start != -1: + depth = 0 + in_string = False + escape = False + for i in range(start, len(text)): + c = text[i] + if escape: + escape = False + continue + if c == "\\": + escape = True + continue + if c == '"' and not escape: + in_string = not in_string + continue + if in_string: + continue + if c == "{": + depth += 1 + elif c == "}": + depth -= 1 + if depth == 0: + candidate = text[start:i + 1] + try: + return json.loads(candidate) + except json.JSONDecodeError: + break + start = text.find("{", start + 1) + return None + + +def try_parse_json(text): + text = str(text or "").strip() + if not text: + raise ValueError("empty_response") + + text = re.sub(r'^```(?:json)?\s*\n?', "", text) + text = re.sub(r'\n?\s*```$', "", text) + + try: + return json.loads(text) + except json.JSONDecodeError: + pass + + sanitized = fix_unicode_escapes(text) + try: + return json.loads(sanitized) + except json.JSONDecodeError: + pass + + for source in (text, sanitized): + result = extract_balanced_json(source) + if result is not None: + return result + + raise ValueError("invalid_response") diff --git a/ai/utils/normalizers.py b/ai/utils/normalizers.py new file mode 100644 index 0000000..96dafef --- /dev/null +++ b/ai/utils/normalizers.py @@ -0,0 +1,57 @@ +import re + +DOI_RE = re.compile(r"\b10\.\d{4,}(?:\.\d+)*\/[^\s\"]+", re.I) +ORCID_RE = re.compile(r"\d{4}-\d{4}-\d{4}-\d{3}[X\d]") + + +def stz_text(value): + return str(value or "").strip() + + +def stz_norm(value): + return re.sub(r"\s+", " ", str(value or "").strip().lower()) + + +def stz_language(code, fallback="en"): + code = (code or "").strip()[:2].lower() + return code if len(code) == 2 and code.isalpha() else fallback + + +def stz_country_code(value): + code = (value or "").strip().upper()[:2] + return code if len(code) == 2 and code.isalpha() else "" + + +def stz_affiliation_id(value): + if isinstance(value, (list, tuple)): + return [str(v) for v in value if v] + if value: + return [str(value)] + return [] + + +def stz_first_number(value): + match = re.search(r"\d+", str(value or "")) + return match.group(0) if match else "" + + +def stz_date(value): + text = stz_text(value) + if not text: + return "" + match = re.match(r"(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?", text) + if not match: + return "" + year, month, day = match.group(1), match.group(2), match.group(3) + if month: + month = str(min(max(int(month), 1), 12)).zfill(2) + if day: + day = str(min(max(int(day), 1), 31)).zfill(2) + parts = [year, month, day] if month else [year] + return "-".join(p for p in parts if p) + + +def stz_year(value): + text = stz_text(value) + match = re.match(r"\d{4}", text) + return match.group(0) if match else "" diff --git a/ai/utils/text.py b/ai/utils/text.py new file mode 100644 index 0000000..4b10dcd --- /dev/null +++ b/ai/utils/text.py @@ -0,0 +1,38 @@ +import logging +import os +import zipfile + +from docling.document_converter import DocumentConverter +from lxml import etree + +logger = logging.getLogger(__name__) + + +def extract_text_from_docx(docx_path, limit_chars=30000): + logger.info("Text extractor: using zipfile for %s", os.path.basename(docx_path)) + with zipfile.ZipFile(docx_path) as z: + xml_bytes = z.read("word/document.xml") + + root = etree.fromstring(xml_bytes) + nsmap = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"} + + paragraphs = [] + for p in root.xpath("//w:p | //w:tc//w:p", namespaces=nsmap): + text = "".join(t.text or "" for t in p.xpath(".//w:t", namespaces=nsmap)) + text = text.strip() + if text: + paragraphs.append(text) + + result = "\n".join(paragraphs)[:limit_chars] + logger.info("Text extractor: zipfile produced %d chars, preview: %s", len(result), result[:200]) + return result + + +def extract_text_from_docx_via_docling(docx_path, limit_chars=30000): + logger.info("Text extractor: using docling for %s", os.path.basename(docx_path)) + converter = DocumentConverter() + result = converter.convert(docx_path) + text = result.document.export_to_text() + result_text = text[:limit_chars] + logger.info("Text extractor: docling produced %d chars, preview: %s", len(result_text), result_text[:200]) + return result_text diff --git a/ai/utils/vision.py b/ai/utils/vision.py new file mode 100644 index 0000000..39ef315 --- /dev/null +++ b/ai/utils/vision.py @@ -0,0 +1,92 @@ +import base64 +import logging +import os +import subprocess +import tempfile + +logger = logging.getLogger(__name__) + + +def _convert_to_pdf(docx_path, pdf_path): + try: + subprocess.run( + [ + "soffice", + "--headless", + "--convert-to", + "pdf", + "--outdir", + os.path.dirname(pdf_path), + docx_path, + ], + capture_output=True, + timeout=60, + ) + generated = os.path.join( + os.path.dirname(pdf_path), + os.path.splitext(os.path.basename(docx_path))[0] + ".pdf", + ) + if os.path.isfile(generated) and generated != pdf_path: + os.rename(generated, pdf_path) + except Exception as exc: + logger.warning("Failed to convert DOCX to PDF: %s", exc) + + +def _pdf_page_count(pdf_path): + try: + result = subprocess.run( + ["pdfinfo", pdf_path], capture_output=True, text=True, timeout=15 + ) + for line in result.stdout.splitlines(): + if line.startswith("Pages:"): + return int(line.split(":")[1].strip()) + except Exception: + pass + return 0 + + +def _pdf_page_to_png(pdf_path, page_num, output_path): + try: + subprocess.run( + [ + "pdftoppm", + "-f", str(page_num), + "-l", str(page_num), + "-r", "120", + "-png", + "-singlefile", + pdf_path, + os.path.splitext(output_path)[0], + ], + capture_output=True, + timeout=30, + ) + except Exception as exc: + logger.warning("Failed to convert PDF page %d to PNG: %s", page_num, exc) + + +def extract_page_images(docx_path, max_pages=3): + with tempfile.TemporaryDirectory() as tmpdir: + pdf_path = os.path.join(tmpdir, "document.pdf") + _convert_to_pdf(docx_path, pdf_path) + if not os.path.isfile(pdf_path): + return [] + + total_pages = _pdf_page_count(pdf_path) + num_pages = min(max_pages, total_pages) + if num_pages == 0: + return [] + + images = [] + for page_num in range(1, num_pages + 1): + png_path = os.path.join(tmpdir, f"page-{page_num}.png") + _pdf_page_to_png(pdf_path, page_num, png_path) + if os.path.isfile(png_path): + with open(png_path, "rb") as f: + b64 = base64.b64encode(f.read()).decode() + images.append(b64) + logger.info("Vision: page %d image %d bytes base64", page_num, len(b64)) + + if images: + logger.info("Vision: %d page images extracted", len(images)) + return images diff --git a/ai/wagtail_hooks.py b/ai/wagtail_hooks.py new file mode 100644 index 0000000..bd56e8e --- /dev/null +++ b/ai/wagtail_hooks.py @@ -0,0 +1,279 @@ +import logging + +import requests as req +from django.contrib import messages +from django.http import HttpResponseRedirect, JsonResponse +from django.urls import path +from django.utils.translation import gettext_lazy as _ +from wagtail import hooks +from wagtail.snippets.models import register_snippet +from wagtail.snippets.views.snippets import ( + CreateView, + EditView, + SnippetViewSet, + SnippetViewSetGroup, +) + +from ai.models import DownloadStatus, GeminiModel, HuggingFaceModel, OllamaModel +from config.menu import get_menu_order + +logger = logging.getLogger(__name__) + + +def ollama_tags(request): + url = request.GET.get("url", "").strip().rstrip("/") + if not url: + return JsonResponse({"error": "URL is required"}, status=400) + try: + logger.info("Fetching Ollama tags from %s/api/tags", url) + resp = req.get(f"{url}/api/tags", timeout=10) + resp.raise_for_status() + data = resp.json() + models = data.get("models", []) + tags = [m["name"] for m in models] + logger.info("Ollama tags: %d found", len(tags)) + return JsonResponse({"models": tags}) + except Exception as e: + logger.error("Ollama tags error: %s", e) + return JsonResponse({"error": str(e)}, status=502) + + +def ollama_model_info(request): + url = request.GET.get("url", "").strip().rstrip("/") + model = request.GET.get("model", "").strip() + if not url or not model: + return JsonResponse({"error": "URL and model are required"}, status=400) + try: + logger.info("Fetching Ollama model info for %s from %s", model, url) + resp = req.post(f"{url}/api/show", json={"name": model}, timeout=15) + resp.raise_for_status() + data = resp.json() + info = { + "context_length": None, + "parameter_size": data.get("details", {}).get("parameter_size"), + } + model_info = data.get("model_info", {}) + for key in model_info: + if "context_length" in key or "num_ctx" in key: + info["context_length"] = model_info[key] + break + logger.info("Ollama model info: %s", info) + return JsonResponse(info) + except Exception as e: + logger.error("Ollama model info error: %s", e) + return JsonResponse({"error": str(e)}, status=502) + + + +class HFModelCreateView(CreateView): + def form_invalid(self, form): + self.produced_error_message = True + return super().form_invalid(form) + + def form_valid(self, form): + self.object = form.save_all(self.request.user) + if self.object.hf_token: + self.object.download_status = DownloadStatus.DOWNLOADING + self.object.save() + from ai.tasks import download_model + download_model.delay(self.object.pk) + messages.success(self.request, _("Model created, download started.")) + else: + messages.success(self.request, _("Model created. Add a token to download.")) + return HttpResponseRedirect(self.get_success_url()) + + +class HFModelEditView(EditView): + def form_invalid(self, form): + self.produced_error_message = True + return super().form_invalid(form) + + def form_valid(self, form): + form.instance.updated_by = self.request.user + form.instance.save() + if form.instance.hf_token and form.instance.download_status != DownloadStatus.DOWNLOADING: + form.instance.download_status = DownloadStatus.DOWNLOADING + form.instance.save() + from ai.tasks import download_model + download_model.delay(form.instance.pk) + messages.success(self.request, _("Download started.")) + else: + messages.success(self.request, _("Model updated.")) + return HttpResponseRedirect(self.get_success_url()) + + +class HuggingFaceViewSet(SnippetViewSet): + model = HuggingFaceModel + add_view_class = HFModelCreateView + edit_view_class = HFModelEditView + menu_label = _("HuggingFace") + menu_icon = "download" + list_display = ("__str__", "get_download_status_display", "is_active") + search_fields = ("name_model", "name_file") + + + +class OllamaModelCreateView(CreateView): + def form_invalid(self, form): + self.produced_error_message = True + return super().form_invalid(form) + + def form_valid(self, form): + self.object = form.save_all(self.request.user) + messages.success(self.request, _("Ollama model created.")) + return HttpResponseRedirect(self.get_success_url()) + + +class OllamaModelEditView(EditView): + def form_invalid(self, form): + self.produced_error_message = True + return super().form_invalid(form) + + def form_valid(self, form): + form.instance.updated_by = self.request.user + form.instance.save() + messages.success(self.request, _("Ollama model updated.")) + return HttpResponseRedirect(self.get_success_url()) + + +class OllamaViewSet(SnippetViewSet): + model = OllamaModel + add_view_class = OllamaModelCreateView + edit_view_class = OllamaModelEditView + menu_label = _("Ollama") + menu_icon = "link-external" + list_display = ("__str__", "url", "is_active") + + + +class GeminiCreateView(CreateView): + def form_invalid(self, form): + self.produced_error_message = True + return super().form_invalid(form) + + def form_valid(self, form): + self.object = form.save_all(self.request.user) + messages.success(self.request, _("Gemini model created.")) + return HttpResponseRedirect(self.get_success_url()) + + +class GeminiEditView(EditView): + def form_invalid(self, form): + self.produced_error_message = True + return super().form_invalid(form) + + def form_valid(self, form): + form.instance.updated_by = self.request.user + form.instance.save() + messages.success(self.request, _("Gemini model updated.")) + return HttpResponseRedirect(self.get_success_url()) + + +class GeminiViewSet(SnippetViewSet): + model = GeminiModel + add_view_class = GeminiCreateView + edit_view_class = GeminiEditView + menu_label = _("Gemini") + menu_icon = "key" + list_display = ("__str__", "is_active") + + + +class AIModelGroup(SnippetViewSetGroup): + menu_name = "ai" + menu_label = _("AI Models") + menu_icon = "ai-brain" + menu_order = get_menu_order("ai") + add_to_settings_menu = True + items = (HuggingFaceViewSet, OllamaViewSet, GeminiViewSet) + + +register_snippet(AIModelGroup) + + + +@hooks.register("register_admin_urls") +def register_ai_admin_urls(): + return [ + path("ai/ollama-tags/", ollama_tags, name="ai_ollama_tags"), + path("ai/ollama-model-info/", ollama_model_info, name="ai_ollama_model_info"), + ] + + + +@hooks.register("insert_global_admin_js") +def ai_model_admin_js(): + return """""" + + +@hooks.register("register_icons") +def register_ai_icons(icons): + return icons + ["wagtailadmin/icons/ai-brain.svg"] diff --git a/compose/local/django/Dockerfile b/compose/local/django/Dockerfile new file mode 100755 index 0000000..bbb3efc --- /dev/null +++ b/compose/local/django/Dockerfile @@ -0,0 +1,103 @@ +ARG PYTHON_VERSION=3.12-bookworm + +# define an alias for the specfic python version used in this file. +FROM python:${PYTHON_VERSION} AS python + +# Python build stage +FROM python AS python-build-stage + +ARG BUILD_ENVIRONMENT=local + +# Install apt packages +RUN apt-get update && apt-get install --no-install-recommends -y \ + # dependencies for building Python packages + build-essential \ + git \ + # psycopg2 dependencies + libpq-dev \ + # other dependencies + software-properties-common \ + libopenblas-dev \ + libomp-dev + +RUN apt-get update && \ + apt-get install -y ninja-build cmake && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +# Actualizar pip, setuptools y wheel antes de instalar dependencias +RUN python -m pip install --upgrade pip setuptools wheel + +# Requirements are installed here to ensure they will be cached. +COPY ./requirements . + +# Update pip +RUN python -m pip install --upgrade pip + +# Create Python Dependency and Sub-Dependency Wheels. +RUN pip wheel --wheel-dir /usr/src/app/wheels \ + -r ${BUILD_ENVIRONMENT}.txt + +# Python 'run' stage +FROM python AS python-run-stage + +ARG BUILD_ENVIRONMENT=local +ARG APP_HOME=/app + +ENV PYTHONUNBUFFERED 1 +ENV PYTHONDONTWRITEBYTECODE 1 +ENV BUILD_ENV ${BUILD_ENVIRONMENT} + +WORKDIR ${APP_HOME} + +RUN if [ -f /etc/apt/sources.list ]; then \ + sed -i 's/main/main contrib non-free/' /etc/apt/sources.list; \ + elif [ -f /etc/apt/sources.list.d/debian.sources ]; then \ + sed -i 's/Components: main/Components: main contrib non-free non-free-firmware/' /etc/apt/sources.list.d/debian.sources; \ + fi + +# Install required system dependencies +RUN apt-get update && apt-get install --no-install-recommends -y \ + # psycopg2 dependencies + libpq-dev \ + # Translations dependencies + gettext \ + # WeasyPrint dependencies + libpango-1.0-0 libpangocairo-1.0-0 shared-mime-info \ + # DOCX to PDF conversion + libreoffice-core libreoffice-writer poppler-utils \ + # cleaning up unused files + && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ + && rm -rf /var/lib/apt/lists/* + +# All absolute dir copies ignore workdir instruction. All relative dir copies are wrt to the workdir instruction +# copy python dependency wheels from python-build-stage +COPY --from=python-build-stage /usr/src/app/wheels /wheels/ + +# Use wheels to install python dependencies +RUN pip install --no-cache-dir --no-index --find-links=/wheels/ $(find /wheels/ -name "*.whl") \ + && rm -rf /wheels/ + +COPY ./compose/production/django/entrypoint /entrypoint +RUN sed -i 's/\r$//g' /entrypoint +RUN chmod +x /entrypoint + +COPY ./compose/local/django/start /start +RUN sed -i 's/\r$//g' /start +RUN chmod +x /start + +COPY ./compose/local/django/celery/worker/start /start-celeryworker +RUN sed -i 's/\r$//g' /start-celeryworker +RUN chmod +x /start-celeryworker + +COPY ./compose/local/django/celery/beat/start /start-celerybeat +RUN sed -i 's/\r$//g' /start-celerybeat +RUN chmod +x /start-celerybeat + +COPY ./compose/local/django/celery/flower/start /start-flower +RUN sed -i 's/\r$//g' /start-flower +RUN chmod +x /start-flower + +# copy application code to WORKDIR +COPY . ${APP_HOME} + +ENTRYPOINT ["/entrypoint"] diff --git a/compose/local/django/Dockerfile.llama b/compose/local/django/Dockerfile.llama new file mode 100644 index 0000000..682c3ce --- /dev/null +++ b/compose/local/django/Dockerfile.llama @@ -0,0 +1,111 @@ +ARG PYTHON_VERSION=3.12-bookworm + +FROM python:${PYTHON_VERSION} AS python + +FROM python AS python-build-stage + +ARG BUILD_ENVIRONMENT=local + +RUN apt-get update && apt-get install --no-install-recommends -y \ + build-essential \ + git \ + libpq-dev \ + software-properties-common \ + libopenblas-dev \ + libomp-dev + +RUN apt-get update && \ + apt-get install -y ninja-build cmake && \ + apt-get clean && rm -rf /var/lib/apt/lists/* + +RUN python -m pip install --upgrade pip setuptools wheel + +COPY ./requirements . + +RUN python -m pip install --upgrade pip + +RUN pip wheel --wheel-dir /usr/src/app/wheels \ + -r ${BUILD_ENVIRONMENT}.txt + +FROM python AS python-run-stage + +ARG BUILD_ENVIRONMENT=local +ARG APP_HOME=/app +ARG DISABLE_AVX=true +ARG LLAMA_VERSION=0.3.14 + +ENV PYTHONUNBUFFERED 1 +ENV PYTHONDONTWRITEBYTECODE 1 +ENV BUILD_ENV ${BUILD_ENVIRONMENT} + +WORKDIR ${APP_HOME} + +RUN if [ -f /etc/apt/sources.list ]; then \ + sed -i 's/main/main contrib non-free/' /etc/apt/sources.list; \ + elif [ -f /etc/apt/sources.list.d/debian.sources ]; then \ + sed -i 's/Components: main/Components: main contrib non-free non-free-firmware/' /etc/apt/sources.list.d/debian.sources; \ + fi + +RUN apt-get update && apt-get install --no-install-recommends -y \ + libpq-dev \ + gettext \ + # WeasyPrint dependencies + libpango-1.0-0 libpangocairo-1.0-0 shared-mime-info \ + build-essential cmake ninja-build \ + # DOCX to image conversion + libreoffice-core libreoffice-writer poppler-utils \ + && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=python-build-stage /usr/src/app/wheels /wheels/ + +RUN pip install --no-cache-dir --no-index --find-links=/wheels/ $(find /wheels/ -name "*.whl" ! -name "llama_cpp_python*") && rm -rf /wheels/ + +ARG TARGETARCH +RUN set -eux; \ + ARCH="${TARGETARCH:-}"; \ + if [ -z "${ARCH}" ]; then ARCH="$(uname -m)"; fi; \ + if [ "${ARCH}" = "arm64" ] || [ "${ARCH}" = "aarch64" ]; then \ + pip install "llama-cpp-python==${LLAMA_VERSION}" \ + --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cpu \ + --only-binary=:all: \ + --no-cache-dir \ + || { \ + export FORCE_CMAKE=1; \ + export CMAKE_ARGS="-DGGML_NATIVE=OFF -DGGML_CPU_ALL_VARIANTS=OFF -DGGML_CPU_ARM_ARCH=armv8-a -DGGML_LLAMAFILE=OFF -DGGML_CPU_REPACK=OFF"; \ + pip install "llama-cpp-python==${LLAMA_VERSION}" --force-reinstall --no-cache-dir; \ + }; \ + else \ + pip install "llama-cpp-python==${LLAMA_VERSION}" --prefer-binary --no-cache-dir \ + || { \ + export FORCE_CMAKE=1; \ + if [ "${DISABLE_AVX}" = "true" ]; then \ + export CMAKE_ARGS="-DLLAMA_AVX=OFF -DLLAMA_AVX2=OFF -DLLAMA_FMA=OFF -DLLAMA_F16C=OFF -DLLAMA_OPENMP=ON"; \ + fi; \ + pip install "llama-cpp-python==${LLAMA_VERSION}" --force-reinstall --no-cache-dir; \ + }; \ + fi + +COPY ./compose/production/django/entrypoint /entrypoint +RUN sed -i 's/\r$//g' /entrypoint +RUN chmod +x /entrypoint + +COPY ./compose/local/django/start /start +RUN sed -i 's/\r$//g' /start +RUN chmod +x /start + +COPY ./compose/local/django/celery/worker/start /start-celeryworker +RUN sed -i 's/\r$//g' /start-celeryworker +RUN chmod +x /start-celeryworker + +COPY ./compose/local/django/celery/beat/start /start-celerybeat +RUN sed -i 's/\r$//g' /start-celerybeat +RUN chmod +x /start-celerybeat + +COPY ./compose/local/django/celery/flower/start /start-flower +RUN sed -i 's/\r$//g' /start-flower +RUN chmod +x /start-flower + +COPY . ${APP_HOME} + +ENTRYPOINT ["/entrypoint"] diff --git a/compose/local/django/celery/beat/start b/compose/local/django/celery/beat/start new file mode 100755 index 0000000..c04a736 --- /dev/null +++ b/compose/local/django/celery/beat/start @@ -0,0 +1,8 @@ +#!/bin/bash + +set -o errexit +set -o nounset + + +rm -f './celerybeat.pid' +celery -A config.celery_app beat -l INFO diff --git a/compose/local/django/celery/flower/start b/compose/local/django/celery/flower/start new file mode 100755 index 0000000..bd3c9f2 --- /dev/null +++ b/compose/local/django/celery/flower/start @@ -0,0 +1,11 @@ +#!/bin/bash + +set -o errexit +set -o nounset + + +celery \ + -A config.celery_app \ + -b "${CELERY_BROKER_URL}" \ + flower \ + --basic_auth="${CELERY_FLOWER_USER}:${CELERY_FLOWER_PASSWORD}" diff --git a/compose/local/django/celery/worker/start b/compose/local/django/celery/worker/start new file mode 100755 index 0000000..75a0677 --- /dev/null +++ b/compose/local/django/celery/worker/start @@ -0,0 +1,10 @@ +#!/bin/bash + +set -o errexit +set -o nounset + + +# Install local packtools in editable mode (dev only - uses bind-mounted workspace) +# pip install -e /packtools --quiet + +watchgod celery.__main__.main --args -A config.celery_app worker -l INFO diff --git a/compose/local/django/start b/compose/local/django/start new file mode 100755 index 0000000..b37814e --- /dev/null +++ b/compose/local/django/start @@ -0,0 +1,12 @@ +#!/bin/bash + +set -o errexit +set -o pipefail +set -o nounset + + +# Install local packtools in editable mode (dev only - uses bind-mounted workspace) +# pip install -e /packtools --quiet + +python manage.py migrate +python manage.py runserver_plus 0.0.0.0:8000 diff --git a/compose/local/docs/Dockerfile b/compose/local/docs/Dockerfile new file mode 100755 index 0000000..1a9ee9d --- /dev/null +++ b/compose/local/docs/Dockerfile @@ -0,0 +1,68 @@ +ARG PYTHON_VERSION=3.9-slim-bullseye + +# define an alias for the specfic python version used in this file. +FROM python:${PYTHON_VERSION} as python + + +# Python build stage +FROM python as python-build-stage + +ENV PYTHONDONTWRITEBYTECODE 1 + +RUN apt-get update && apt-get install --no-install-recommends -y \ + # dependencies for building Python packages + build-essential \ + # psycopg2 dependencies + libpq-dev \ + git \ + # cleaning up unused files + && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ + && rm -rf /var/lib/apt/lists/* + +# Requirements are installed here to ensure they will be cached. +COPY ./requirements /requirements + +# Update pip +RUN python -m pip install --upgrade pip + +# create python dependency wheels +RUN pip wheel --no-cache-dir --no-deps --wheel-dir /usr/src/app/wheels \ + -r /requirements/local.txt -r /requirements/production.txt \ + && rm -rf /requirements + + +# Python 'run' stage +FROM python as python-run-stage + +ARG BUILD_ENVIRONMENT +ENV PYTHONUNBUFFERED 1 +ENV PYTHONDONTWRITEBYTECODE 1 + +RUN apt-get update && apt-get install --no-install-recommends -y \ + # To run the Makefile + make \ + # psycopg2 dependencies + libpq-dev \ + # Translations dependencies + gettext \ + # Uncomment below lines to enable Sphinx output to latex and pdf + # texlive-latex-recommended \ + # texlive-fonts-recommended \ + # texlive-latex-extra \ + # latexmk \ + # cleaning up unused files + && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ + && rm -rf /var/lib/apt/lists/* + +# copy python dependency wheels from python-build-stage +COPY --from=python-build-stage /usr/src/app/wheels /wheels + +# use wheels to install python dependencies +RUN pip install --no-cache /wheels/* \ + && rm -rf /wheels + +COPY ./compose/local/docs/start /start-docs +RUN sed -i 's/\r$//g' /start-docs +RUN chmod +x /start-docs + +WORKDIR /docs diff --git a/compose/local/docs/start b/compose/local/docs/start new file mode 100755 index 0000000..fd2e0de --- /dev/null +++ b/compose/local/docs/start @@ -0,0 +1,7 @@ +#!/bin/bash + +set -o errexit +set -o pipefail +set -o nounset + +make livehtml diff --git a/compose/production/django/Dockerfile b/compose/production/django/Dockerfile new file mode 100755 index 0000000..e80b9c2 --- /dev/null +++ b/compose/production/django/Dockerfile @@ -0,0 +1,99 @@ +ARG PYTHON_VERSION=3.12-bookworm + +# define an alias for the specfic python version used in this file. +FROM python:${PYTHON_VERSION} as python + +# Python build stage +FROM python as python-build-stage + +ARG BUILD_ENVIRONMENT=production + +# Install apt packages +RUN apt-get update && apt-get install --no-install-recommends -y \ + # dependencies for building Python packages + git \ + build-essential \ + # psycopg2 dependencies + libpq-dev + +# Requirements are installed here to ensure they will be cached. +COPY ./requirements . + +# Update pip +RUN python -m pip install --upgrade pip + +# Create Python Dependency and Sub-Dependency Wheels. +RUN pip wheel --wheel-dir /usr/src/app/wheels \ + -r ${BUILD_ENVIRONMENT}.txt + +# Python 'run' stage +FROM python as python-run-stage + +ARG BUILD_ENVIRONMENT=production +ARG APP_HOME=/app + +ENV PYTHONUNBUFFERED 1 +ENV PYTHONDONTWRITEBYTECODE 1 +ENV BUILD_ENV ${BUILD_ENVIRONMENT} + +WORKDIR ${APP_HOME} + +RUN addgroup --system django \ + && adduser --system --ingroup django django + +RUN if [ -f /etc/apt/sources.list ]; then \ + sed -i 's/main/main contrib non-free/' /etc/apt/sources.list; \ + elif [ -f /etc/apt/sources.list.d/debian.sources ]; then \ + sed -i 's/Components: main/Components: main contrib non-free non-free-firmware/' /etc/apt/sources.list.d/debian.sources; \ + fi + +# Install required system dependencies +RUN apt-get update && apt-get install --no-install-recommends -y \ + # psycopg2 dependencies + libpq-dev \ + # WeasyPrint dependencies + libpango-1.0-0 libpangocairo-1.0-0 shared-mime-info \ + # Translations dependencies + gettext \ + # DOCX to PDF conversion + libreoffice-core libreoffice-writer poppler-utils \ + # cleaning up unused files + && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ + && rm -rf /var/lib/apt/lists/* + +# All absolute dir copies ignore workdir instruction. All relative dir copies are wrt to the workdir instruction +# copy python dependency wheels from python-build-stage +COPY --from=python-build-stage /usr/src/app/wheels /wheels/ + +# use wheels to install python dependencies +RUN pip install --no-cache-dir --no-index --find-links=/wheels/ $(find /wheels/ -name "*.whl") && rm -rf /wheels/ + +COPY --chown=django:django ./compose/production/django/entrypoint /entrypoint +RUN sed -i 's/\r$//g' /entrypoint +RUN chmod +x /entrypoint + +COPY --chown=django:django ./compose/production/django/start /start +RUN sed -i 's/\r$//g' /start +RUN chmod +x /start + +COPY --chown=django:django ./compose/production/django/celery/worker/start /start-celeryworker +RUN sed -i 's/\r$//g' /start-celeryworker +RUN chmod +x /start-celeryworker + +COPY --chown=django:django ./compose/production/django/celery/beat/start /start-celerybeat +RUN sed -i 's/\r$//g' /start-celerybeat +RUN chmod +x /start-celerybeat + +COPY ./compose/production/django/celery/flower/start /start-flower +RUN sed -i 's/\r$//g' /start-flower +RUN chmod +x /start-flower + +# copy application code to WORKDIR +COPY --chown=django:django . ${APP_HOME} + +# make django owner of the WORKDIR directory as well. +RUN chown django:django ${APP_HOME} + +USER django + +ENTRYPOINT ["/entrypoint"] diff --git a/compose/production/django/Dockerfile.llama b/compose/production/django/Dockerfile.llama new file mode 100755 index 0000000..de81276 --- /dev/null +++ b/compose/production/django/Dockerfile.llama @@ -0,0 +1,110 @@ +ARG PYTHON_VERSION=3.12-bookworm + +# define an alias for the specfic python version used in this file. +FROM python:${PYTHON_VERSION} as python + +# Python build stage +FROM python as python-build-stage + +ARG BUILD_ENVIRONMENT=production + +# Install apt packages +RUN apt-get update && apt-get install --no-install-recommends -y \ + # dependencies for building Python packages + git \ + build-essential \ + # psycopg2 dependencies + libpq-dev + +# Requirements are installed here to ensure they will be cached. +COPY ./requirements . + +# Update pip +RUN python -m pip install --upgrade pip + +# Create Python Dependency and Sub-Dependency Wheels. +RUN pip wheel --wheel-dir /usr/src/app/wheels \ + -r ${BUILD_ENVIRONMENT}.txt + +# Python 'run' stage +FROM python as python-run-stage + +ARG BUILD_ENVIRONMENT=production +ARG APP_HOME=/app + +ENV PYTHONUNBUFFERED 1 +ENV PYTHONDONTWRITEBYTECODE 1 +ENV BUILD_ENV ${BUILD_ENVIRONMENT} + +# Install llama-cpp-python with specific CMAKE flags for Kubernetes nodes without AVX support +ARG DISABLE_AVX=true + +# Set the version of llama-cpp-python +ARG LLAMA_VERSION=0.3.14 + +WORKDIR ${APP_HOME} + +RUN addgroup --system django \ + && adduser --system --ingroup django django + +RUN if [ -f /etc/apt/sources.list ]; then \ + sed -i 's/main/main contrib non-free/' /etc/apt/sources.list; \ + elif [ -f /etc/apt/sources.list.d/debian.sources ]; then \ + sed -i 's/Components: main/Components: main contrib non-free non-free-firmware/' /etc/apt/sources.list.d/debian.sources; \ + fi + +# Install required system dependencies +RUN apt-get update && apt-get install --no-install-recommends -y \ + # psycopg2 dependencies + libpq-dev \ + # WeasyPrint dependencies + libpango-1.0-0 libpangocairo-1.0-0 shared-mime-info \ + # Translations dependencies + gettext \ + # cleaning up unused files + && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ + && rm -rf /var/lib/apt/lists/* + +# All absolute dir copies ignore workdir instruction. All relative dir copies are wrt to the workdir instruction +# copy python dependency wheels from python-build-stage +COPY --from=python-build-stage /usr/src/app/wheels /wheels/ + +# use wheels to install python dependencies (excluding llama-cpp-python) +RUN pip install --no-cache-dir --no-index --find-links=/wheels/ $(find /wheels/ -name "*.whl" ! -name "llama_cpp_python*") && rm -rf /wheels/ + +# Install llama-cpp-python with specific CMAKE flags for Kubernetes nodes without AVX support +RUN if [ "${DISABLE_AVX}" = "true" ]; then \ + CMAKE_ARGS='-DLLAMA_AVX=OFF -DLLAMA_AVX2=OFF -DLLAMA_FMA=OFF -DLLAMA_F16C=OFF -DLLAMA_OPENMP=ON' pip install llama-cpp-python==${LLAMA_VERSION} --force-reinstall --no-cache-dir; \ + else \ + pip install llama-cpp-python==${LLAMA_VERSION} --force-reinstall --no-cache-dir; \ + fi + +COPY --chown=django:django ./compose/production/django/entrypoint /entrypoint +RUN sed -i 's/\r$//g' /entrypoint +RUN chmod +x /entrypoint + +COPY --chown=django:django ./compose/production/django/start /start +RUN sed -i 's/\r$//g' /start +RUN chmod +x /start + +COPY --chown=django:django ./compose/production/django/celery/worker/start /start-celeryworker +RUN sed -i 's/\r$//g' /start-celeryworker +RUN chmod +x /start-celeryworker + +COPY --chown=django:django ./compose/production/django/celery/beat/start /start-celerybeat +RUN sed -i 's/\r$//g' /start-celerybeat +RUN chmod +x /start-celerybeat + +COPY ./compose/production/django/celery/flower/start /start-flower +RUN sed -i 's/\r$//g' /start-flower +RUN chmod +x /start-flower + +# copy application code to WORKDIR +COPY --chown=django:django . ${APP_HOME} + +# make django owner of the WORKDIR directory as well. +RUN chown django:django ${APP_HOME} + +USER django + +ENTRYPOINT ["/entrypoint"] diff --git a/compose/production/django/celery/beat/start b/compose/production/django/celery/beat/start new file mode 100755 index 0000000..42ddca9 --- /dev/null +++ b/compose/production/django/celery/beat/start @@ -0,0 +1,8 @@ +#!/bin/bash + +set -o errexit +set -o pipefail +set -o nounset + + +exec celery -A config.celery_app beat -l INFO diff --git a/compose/production/django/celery/flower/start b/compose/production/django/celery/flower/start new file mode 100755 index 0000000..4180d67 --- /dev/null +++ b/compose/production/django/celery/flower/start @@ -0,0 +1,11 @@ +#!/bin/bash + +set -o errexit +set -o nounset + + +exec celery \ + -A config.celery_app \ + -b "${CELERY_BROKER_URL}" \ + flower \ + --basic_auth="${CELERY_FLOWER_USER}:${CELERY_FLOWER_PASSWORD}" diff --git a/compose/production/django/celery/worker/start b/compose/production/django/celery/worker/start new file mode 100755 index 0000000..af0c8f7 --- /dev/null +++ b/compose/production/django/celery/worker/start @@ -0,0 +1,8 @@ +#!/bin/bash + +set -o errexit +set -o pipefail +set -o nounset + + +exec celery -A config.celery_app worker -l INFO diff --git a/compose/production/django/entrypoint b/compose/production/django/entrypoint new file mode 100755 index 0000000..599841e --- /dev/null +++ b/compose/production/django/entrypoint @@ -0,0 +1,45 @@ +#!/bin/bash + +set -o errexit +set -o pipefail +set -o nounset + + + +# N.B. If only .env files supported variable expansion... +export CELERY_BROKER_URL="${REDIS_URL}" + + +if [ -z "${POSTGRES_USER}" ]; then + base_postgres_image_default_user='postgres' + export POSTGRES_USER="${base_postgres_image_default_user}" +fi +export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" + +postgres_ready() { +python << END +import sys + +import psycopg2 + +try: + psycopg2.connect( + dbname="${POSTGRES_DB}", + user="${POSTGRES_USER}", + password="${POSTGRES_PASSWORD}", + host="${POSTGRES_HOST}", + port="${POSTGRES_PORT}", + ) +except psycopg2.OperationalError: + sys.exit(-1) +sys.exit(0) + +END +} +until postgres_ready; do + >&2 echo 'Waiting for PostgreSQL to become available...' + sleep 1 +done +>&2 echo 'PostgreSQL is available' + +exec "$@" diff --git a/compose/production/django/start b/compose/production/django/start new file mode 100755 index 0000000..d9da8ea --- /dev/null +++ b/compose/production/django/start @@ -0,0 +1,29 @@ +#!/bin/bash + +set -o errexit +set -o pipefail +set -o nounset + + +python /app/manage.py collectstatic --noinput + +compress_enabled() { +python << END +import sys + +from environ import Env + +env = Env(COMPRESS_ENABLED=(bool, True)) +if env('COMPRESS_ENABLED'): + sys.exit(0) +else: + sys.exit(1) + +END +} + +if compress_enabled; then + # NOTE this command will fail if django-compressor is disabled + python /app/manage.py compress +fi +/usr/local/bin/gunicorn config.wsgi --bind 0.0.0.0:5000 --chdir=/app --timeout 1000 --workers 3 --worker-connections=1000 --worker-class=gevent diff --git a/compose/production/postgres/Dockerfile b/compose/production/postgres/Dockerfile new file mode 100755 index 0000000..8b3ebd3 --- /dev/null +++ b/compose/production/postgres/Dockerfile @@ -0,0 +1,6 @@ +FROM postgres:18.4 + +COPY ./compose/production/postgres/maintenance /usr/local/bin/maintenance +RUN chmod +x /usr/local/bin/maintenance/* +RUN mv /usr/local/bin/maintenance/* /usr/local/bin \ + && rmdir /usr/local/bin/maintenance diff --git a/compose/production/postgres/maintenance/_sourced/constants.sh b/compose/production/postgres/maintenance/_sourced/constants.sh new file mode 100755 index 0000000..6ca4f0c --- /dev/null +++ b/compose/production/postgres/maintenance/_sourced/constants.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + + +BACKUP_DIR_PATH='/backups' +BACKUP_FILE_PREFIX='backup' diff --git a/compose/production/postgres/maintenance/_sourced/countdown.sh b/compose/production/postgres/maintenance/_sourced/countdown.sh new file mode 100755 index 0000000..e6cbfb6 --- /dev/null +++ b/compose/production/postgres/maintenance/_sourced/countdown.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + + +countdown() { + declare desc="A simple countdown. Source: https://superuser.com/a/611582" + local seconds="${1}" + local d=$(($(date +%s) + "${seconds}")) + while [ "$d" -ge `date +%s` ]; do + echo -ne "$(date -u --date @$(($d - `date +%s`)) +%H:%M:%S)\r"; + sleep 0.1 + done +} diff --git a/compose/production/postgres/maintenance/_sourced/messages.sh b/compose/production/postgres/maintenance/_sourced/messages.sh new file mode 100755 index 0000000..f6be756 --- /dev/null +++ b/compose/production/postgres/maintenance/_sourced/messages.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash + + +message_newline() { + echo +} + +message_debug() +{ + echo -e "DEBUG: ${@}" +} + +message_welcome() +{ + echo -e "\e[1m${@}\e[0m" +} + +message_warning() +{ + echo -e "\e[33mWARNING\e[0m: ${@}" +} + +message_error() +{ + echo -e "\e[31mERROR\e[0m: ${@}" +} + +message_info() +{ + echo -e "\e[37mINFO\e[0m: ${@}" +} + +message_suggestion() +{ + echo -e "\e[33mSUGGESTION\e[0m: ${@}" +} + +message_success() +{ + echo -e "\e[32mSUCCESS\e[0m: ${@}" +} diff --git a/compose/production/postgres/maintenance/_sourced/yes_no.sh b/compose/production/postgres/maintenance/_sourced/yes_no.sh new file mode 100755 index 0000000..fd9cae1 --- /dev/null +++ b/compose/production/postgres/maintenance/_sourced/yes_no.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + + +yes_no() { + declare desc="Prompt for confirmation. \$\"\{1\}\": confirmation message." + local arg1="${1}" + + local response= + read -r -p "${arg1} (y/[n])? " response + if [[ "${response}" =~ ^[Yy]$ ]] + then + exit 0 + else + exit 1 + fi +} diff --git a/compose/production/postgres/maintenance/backup b/compose/production/postgres/maintenance/backup new file mode 100755 index 0000000..ee0c9d6 --- /dev/null +++ b/compose/production/postgres/maintenance/backup @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + + +### Create a database backup. +### +### Usage: +### $ docker-compose -f .yml (exec |run --rm) postgres backup + + +set -o errexit +set -o pipefail +set -o nounset + + +working_dir="$(dirname ${0})" +source "${working_dir}/_sourced/constants.sh" +source "${working_dir}/_sourced/messages.sh" + + +message_welcome "Backing up the '${POSTGRES_DB}' database..." + + +if [[ "${POSTGRES_USER}" == "postgres" ]]; then + message_error "Backing up as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." + exit 1 +fi + +export PGHOST="${POSTGRES_HOST}" +export PGPORT="${POSTGRES_PORT}" +export PGUSER="${POSTGRES_USER}" +export PGPASSWORD="${POSTGRES_PASSWORD}" +export PGDATABASE="${POSTGRES_DB}" + +backup_filename="${BACKUP_FILE_PREFIX}_$(date +'%Y_%m_%dT%H_%M_%S').sql.gz" +pg_dump | gzip > "${BACKUP_DIR_PATH}/${backup_filename}" + + +message_success "'${POSTGRES_DB}' database backup '${backup_filename}' has been created and placed in '${BACKUP_DIR_PATH}'." diff --git a/compose/production/postgres/maintenance/backups b/compose/production/postgres/maintenance/backups new file mode 100755 index 0000000..0484ccf --- /dev/null +++ b/compose/production/postgres/maintenance/backups @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + + +### View backups. +### +### Usage: +### $ docker-compose -f .yml (exec |run --rm) postgres backups + + +set -o errexit +set -o pipefail +set -o nounset + + +working_dir="$(dirname ${0})" +source "${working_dir}/_sourced/constants.sh" +source "${working_dir}/_sourced/messages.sh" + + +message_welcome "These are the backups you have got:" + +ls -lht "${BACKUP_DIR_PATH}" diff --git a/compose/production/postgres/maintenance/restore b/compose/production/postgres/maintenance/restore new file mode 100755 index 0000000..9661ca7 --- /dev/null +++ b/compose/production/postgres/maintenance/restore @@ -0,0 +1,55 @@ +#!/usr/bin/env bash + + +### Restore database from a backup. +### +### Parameters: +### <1> filename of an existing backup. +### +### Usage: +### $ docker-compose -f .yml (exec |run --rm) postgres restore <1> + + +set -o errexit +set -o pipefail +set -o nounset + + +working_dir="$(dirname ${0})" +source "${working_dir}/_sourced/constants.sh" +source "${working_dir}/_sourced/messages.sh" + + +if [[ -z ${1+x} ]]; then + message_error "Backup filename is not specified yet it is a required parameter. Make sure you provide one and try again." + exit 1 +fi +backup_filename="${BACKUP_DIR_PATH}/${1}" +if [[ ! -f "${backup_filename}" ]]; then + message_error "No backup with the specified filename found. Check out the 'backups' maintenance script output to see if there is one and try again." + exit 1 +fi + +message_welcome "Restoring the '${POSTGRES_DB}' database from the '${backup_filename}' backup..." + +if [[ "${POSTGRES_USER}" == "postgres" ]]; then + message_error "Restoring as 'postgres' user is not supported. Assign 'POSTGRES_USER' env with another one and try again." + exit 1 +fi + +export PGHOST="${POSTGRES_HOST}" +export PGPORT="${POSTGRES_PORT}" +export PGUSER="${POSTGRES_USER}" +export PGPASSWORD="${POSTGRES_PASSWORD}" +export PGDATABASE="${POSTGRES_DB}" + +message_info "Dropping the database..." +dropdb "${PGDATABASE}" + +message_info "Creating a new database..." +createdb --owner="${POSTGRES_USER}" + +message_info "Applying the backup to the new database..." +gunzip -c "${backup_filename}" | psql "${POSTGRES_DB}" + +message_success "The '${POSTGRES_DB}' database has been restored from the '${backup_filename}' backup." diff --git a/compose/production/traefik/Dockerfile b/compose/production/traefik/Dockerfile new file mode 100755 index 0000000..1becd44 --- /dev/null +++ b/compose/production/traefik/Dockerfile @@ -0,0 +1,5 @@ +FROM traefik:v3.7.1 +RUN mkdir -p /etc/traefik/acme \ + && touch /etc/traefik/acme/acme.json \ + && chmod 600 /etc/traefik/acme/acme.json +COPY ./compose/production/traefik/traefik.yml /etc/traefik diff --git a/compose/production/traefik/traefik.yml b/compose/production/traefik/traefik.yml new file mode 100755 index 0000000..510adc1 --- /dev/null +++ b/compose/production/traefik/traefik.yml @@ -0,0 +1,75 @@ +log: + level: INFO + +entryPoints: + web: + # http + address: ":80" + http: + # https://docs.traefik.io/routing/entrypoints/#entrypoint + redirections: + entryPoint: + to: web-secure + + web-secure: + # https + address: ":443" + + flower: + address: ":5555" + +certificatesResolvers: + letsencrypt: + # https://docs.traefik.io/master/https/acme/#lets-encrypt + acme: + email: "atta.jamil@innolabs.com.br" + storage: /etc/traefik/acme/acme.json + # https://docs.traefik.io/master/https/acme/#httpchallenge + httpChallenge: + entryPoint: web + +http: + routers: + web-secure-router: + rule: "Host(`example.com`) || Host(`www.example.com`)" + entryPoints: + - web-secure + middlewares: + - csrf + service: django + tls: + # https://docs.traefik.io/master/routing/routers/#certresolver + certResolver: letsencrypt + + flower-secure-router: + rule: "Host(`example.com`)" + entryPoints: + - flower + service: flower + tls: + # https://docs.traefik.io/master/routing/routers/#certresolver + certResolver: letsencrypt + + middlewares: + csrf: + # https://docs.traefik.io/master/middlewares/headers/#hostsproxyheaders + # https://docs.djangoproject.com/en/dev/ref/csrf/#ajax + headers: + hostsProxyHeaders: ["X-CSRFToken"] + + services: + django: + loadBalancer: + servers: + - url: http://django:5000 + + flower: + loadBalancer: + servers: + - url: http://flower:5555 + +providers: + # https://docs.traefik.io/master/providers/file/ + file: + filename: /etc/traefik/traefik.yml + watch: true diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..36a4501 --- /dev/null +++ b/config/__init__.py @@ -0,0 +1,5 @@ +# This will make sure the app is always imported when +# Django starts so that shared_task will use this app. +from .celery_app import app as celery_app + +__all__ = ("celery_app",) \ No newline at end of file diff --git a/config/api_router.py b/config/api_router.py new file mode 100644 index 0000000..269e9e6 --- /dev/null +++ b/config/api_router.py @@ -0,0 +1,15 @@ +from django.conf import settings +from rest_framework.routers import DefaultRouter, SimpleRouter + +from manuscripts.api.v1.views import ArticleViewSet +from references.api.v1.views import ReferenceViewSet + +if settings.DEBUG: + router = DefaultRouter() +else: + router = SimpleRouter() + +router.register("references", ReferenceViewSet, basename="references") +router.register("first_block", ArticleViewSet, basename="first_block") + +urlpatterns = router.urls \ No newline at end of file diff --git a/config/celery_app.py b/config/celery_app.py new file mode 100644 index 0000000..f821f5d --- /dev/null +++ b/config/celery_app.py @@ -0,0 +1,17 @@ +import os + +from celery import Celery + +# set the default Django settings module for the 'celery' program. +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") + +app = Celery("core") + +# Using a string here means the worker doesn't have to serialize +# the configuration object to child processes. +# - namespace='CELERY' means all celery-related configuration keys +# should have a `CELERY_` prefix. +app.config_from_object("django.conf:settings", namespace="CELERY") + +# Load task modules from all registered Django app configs. +app.autodiscover_tasks() diff --git a/config/menu.py b/config/menu.py new file mode 100644 index 0000000..4864b87 --- /dev/null +++ b/config/menu.py @@ -0,0 +1,27 @@ +WAGTAIL_MENU_GROUPS_ORDER = [ + "manuscripts", + "references", + "xml_manager", + "journals", + "ai", + "celery_wagtail", +] + + +def get_menu_order(app_name): + try: + return WAGTAIL_MENU_GROUPS_ORDER.index(app_name) + 1 + except ValueError: + return 9000 + + +MANUSCRIPTS_SUBMENU_ORDER = { + "upload": 1, + "processed": 2, + "xml_editor": 3, + "issue": 4, +} + + +def get_manuscripts_submenu_order(item_name): + return MANUSCRIPTS_SUBMENU_ORDER.get(item_name, 9000) diff --git a/config/settings/__init__.py b/config/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/settings/base.py b/config/settings/base.py new file mode 100644 index 0000000..5b922b3 --- /dev/null +++ b/config/settings/base.py @@ -0,0 +1,353 @@ +""" +Django settings for scielo_tools project. + +Generated by 'django-admin startproject' using Django 5.1.3. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.1/ref/settings/ +""" + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) + +import os +from datetime import timedelta +from pathlib import Path + +import environ + +PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BASE_DIR = os.path.dirname(PROJECT_DIR) + +ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent.parent +# core/ +APPS_DIR = ROOT_DIR / "core" +LLAMA_MODEL_DIR = ROOT_DIR / "ai/download" + +env = environ.Env() +READ_DOT_ENV_FILE = env.bool("DJANGO_READ_DOT_ENV_FILE", default=False) +if READ_DOT_ENV_FILE: + # OS environment variables take precedence over variables from .env + env.read_env(str(ROOT_DIR / ".env")) + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/ + + +# Application definition + +WAGTAIL = [ + "core.home", + "core.search", + "wagtail.contrib.forms", + "wagtail.contrib.redirects", + 'wagtail.contrib.settings', + "wagtail_modeladmin", + "wagtail.embeds", + "wagtail.sites", + "wagtail.users", + "wagtail.snippets", + "wagtail.documents", + "wagtail.images", + "wagtail.search", + "wagtail.admin", + "wagtail", + "modelcluster", + "taggit", +] + +DJANGO_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.postgres", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django_celery_results" +] + +THIRD_PARTY_APPS = [ + "compressor", + "wagtailautocomplete", + "wagtail_json_widget", + "django_celery_beat", +] + +LOCAL_APPS = [ + "users", + "core", + "core_settings", + "references", + "sps", + "labeling", + "ai", + "journals", + "manuscripts", +] + +INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS + WAGTAIL + +MIDDLEWARE = [ + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + 'django.middleware.locale.LocaleMiddleware', + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django.middleware.security.SecurityMiddleware", + "wagtail.contrib.redirects.middleware.RedirectMiddleware", +] + +ROOT_URLCONF = "config.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [str(APPS_DIR / "templates")], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + 'wagtail.contrib.settings.context_processors.settings', + ], + }, + }, +] + +WSGI_APPLICATION = "config.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/5.1/ref/settings/#databases +DATABASES = {"default": env.db("DATABASE_URL")} +DATABASES["default"]["ATOMIC_REQUESTS"] = True + + +# Password validation +# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + +# Internationalization +# https://docs.djangoproject.com/en/5.1/topics/i18n/ + +LANGUAGE_CODE = "en" + +LANGUAGES = [ + ('pt-br', 'Português (Brasil)'), + ('es', 'Español'), + ('en', 'English'), +] + +LOCALE_PATHS = [ + os.path.join(BASE_DIR, 'locale'), +] + +TIME_ZONE = "UTC" + +USE_I18N = True +USE_L10N = True +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.1/howto/static-files/ + +STATICFILES_FINDERS = [ + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", +] + +# STATIC +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#static-root +STATIC_ROOT = str(ROOT_DIR / "staticfiles") +# https://docs.djangoproject.com/en/dev/ref/settings/#static-url +STATIC_URL = "/static/" +# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#std:setting-STATICFILES_DIRS +STATICFILES_DIRS = [str(APPS_DIR / "static")] +# https://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/#staticfiles-finders +STATICFILES_FINDERS = [ + "django.contrib.staticfiles.finders.FileSystemFinder", + "django.contrib.staticfiles.finders.AppDirectoriesFinder", +] + +STATICFILES_FINDERS += ["compressor.finders.CompressorFinder"] + +# MEDIA +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#media-root +MEDIA_ROOT = str(APPS_DIR / "media") +# https://docs.djangoproject.com/en/dev/ref/settings/#media-url +MEDIA_URL = "/media/" + + +# Default storage settings, with the staticfiles storage updated. +# See https://docs.djangoproject.com/en/5.1/ref/settings/#std-setting-STORAGES +STORAGES = { + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, + # ManifestStaticFilesStorage is recommended in production, to prevent + # outdated JavaScript / CSS assets being served from cache + # (e.g. after a Wagtail upgrade). + # See https://docs.djangoproject.com/en/5.1/ref/contrib/staticfiles/#manifeststaticfilesstorage + "staticfiles": { + "BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage", + }, +} + + +# Wagtail settings + +WAGTAIL_SITE_NAME = "SciELO Tools" + +# Search +# https://docs.wagtail.org/en/stable/topics/search/backends.html +WAGTAILSEARCH_BACKENDS = { + "default": { + "BACKEND": "wagtail.search.backends.database", + } +} + +# Base URL to use when referring to full URLs within the Wagtail admin backend - +# e.g. in notification emails. Don't include '/admin' or a trailing slash +WAGTAILADMIN_BASE_URL = "http://tools.scielo.org" + +WAGTAILIMAGES_EXTENSIONS = [ + "avif", + "gif", + "jpg", + "jpeg", + "png", + "webp", + "svg", +] + +# Allowed file extensions for documents in the document library. +# This can be omitted to allow all files, but note that this may present a security risk +# if untrusted users are allowed to upload files - +# see https://docs.wagtail.org/en/stable/advanced_topics/deploying.html#user-uploaded-files +WAGTAILDOCS_EXTENSIONS = ['csv', 'docx', 'json', 'key', 'odt', 'pdf', 'pptx', 'rtf', 'txt', 'xlsx', 'zip'] + +# https://docs.djangoproject.com/en/dev/ref/settings/#auth-user-model +AUTH_USER_MODEL = 'users.CustomUser' + +# Celery +# ------------------------------------------------------------------------------ +if USE_TZ: + # http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-timezone + CELERY_TIMEZONE = TIME_ZONE +# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-broker_url +CELERY_BROKER_URL = env("CELERY_BROKER_URL") +# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_backend +CELERY_RESULT_BACKEND = CELERY_BROKER_URL +# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-accept_content +CELERY_ACCEPT_CONTENT = ["json"] +# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-task_serializer +CELERY_TASK_SERIALIZER = "json" +# http://docs.celeryproject.org/en/latest/userguide/configuration.html#std:setting-result_serializer +CELERY_RESULT_SERIALIZER = "json" +# http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-time-limit +# TODO: set to whatever value is adequate in your circumstances +CELERY_TASK_TIME_LIMIT = 5 * 60 +# http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-soft-time-limit +# TODO: set to whatever value is adequate in your circumstances +CELERY_TASK_SOFT_TIME_LIMIT = 36000 +# http://docs.celeryproject.org/en/latest/userguide/configuration.html#beat-scheduler +CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" +# http://docs.celeryproject.org/en/latest/userguide/configuration.html +DJANGO_CELERY_BEAT_TZ_AWARE = False +#CELERY PROMETHEUS DASHBOARD +# https://docs.celeryq.dev/en/stable/userguide/configuration.html#worker-send-task-events +CELERY_WORKER_SEND_TASK_EVENTS = True +# https://docs.celeryq.dev/en/stable/userguide/configuration.html#std-setting-task_send_sent_event +CELERY_SEND_TASK_SENT_EVENT = True +CELERYD_SEND_EVENTS = True +CE_BUCKETS=1,2.5,5,10,30,60,300,600,900,1800 + +# Celery Results +# ------------------------------------------------------------------------------ +# https: // django-celery-results.readthedocs.io/en/latest/getting_started.html +CELERY_RESULT_BACKEND = "django-db" +CELERY_CACHE_BACKEND = "django-cache" +CELERY_RESULT_EXTENDED = True + +# django rest-framework +# ------------------------------------------------------------------------------ +REST_FRAMEWORK = { + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": env.int("DRF_PAGE_SIZE", default=10), + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_simplejwt.authentication.JWTAuthentication", + ), +} +# JWT +SIMPLE_JWT = { + "AUTH_HEADER_TYPES": ("Bearer",), # na doc está JWT mas pode mudar pra Bearer. + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), + "REFRESH_TOKEN_LIFETIME": timedelta(days=1), + # "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), +} + +# LLAMA +LLAMA_ENABLED = env.bool("LLAMA_ENABLED", default=True) +MODEL_LLAMA = "llama-3.2-3b-instruct-q4_k_m.gguf" + +# HTML generation +HTML_GENERATION_CONFIG = { + "valid_only": False, + "xslt": "3.0", + "output_style": "website", + "css": "/static/css/article.css", + "print_css": "", + "js": "/static/js/scielo-bundle-min.js", + "bootstrap_css": "/static/css/bootstrap.min.css", + "article_css": "/static/css/article.css", + "math_elem_preference": "mml:math", + "math_js": "https://cdn.jsdelivr.net/npm/mathjax@3.0.0/es5/tex-mml-svg.js", +} + +# Core API +CORE_API_DOMAIN = env("CORE_API_DOMAIN", default="https://core.scielo.org") +CORE_COLLECTION_API_ENDPOINT = env( + "CORE_COLLECTION_API_ENDPOINT", + default="/api/v2/pid/collection/", +) +CORE_JOURNAL_API_ENDPOINT = env( + "CORE_JOURNAL_API_ENDPOINT", + default="/api/v2/pid/journal/", +) +CORE_ISSUE_API_ENDPOINT = env( + "CORE_ISSUE_API_ENDPOINT", + default="/api/v1/issue/", +) +CORE_ISSUE_FROM_DATE_CREATED = env( + "CORE_ISSUE_FROM_DATE_CREATED", + default="2019-01-01", +) +CORE_COLLECTION_API_URL = f"{CORE_API_DOMAIN}{CORE_COLLECTION_API_ENDPOINT}" +CORE_JOURNAL_API_URL = f"{CORE_API_DOMAIN}{CORE_JOURNAL_API_ENDPOINT}" + +#Aumento en el límite de campos +DATA_UPLOAD_MAX_NUMBER_FIELDS = 10000 diff --git a/config/settings/local.py b/config/settings/local.py new file mode 100644 index 0000000..33ed7c8 --- /dev/null +++ b/config/settings/local.py @@ -0,0 +1,68 @@ +from .base import * + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = env( + "DJANGO_SECRET_KEY", + default="django-insecure-local-dev-only", +) + +# CACHES +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#caches +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.redis.RedisCache", + "LOCATION": "redis://redis:6379", + } +} + +# SECURITY WARNING: define the correct hosts in production! +ALLOWED_HOSTS = ["*"] +CSRF_TRUSTED_ORIGINS = env.list( + "DJANGO_CSRF_TRUSTED_ORIGINS", + default=["https://tools-hml.scielo.org"], +) + +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + +# EMAIL +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#email-host +EMAIL_HOST = env("EMAIL_HOST", default="mailhog") +# https://docs.djangoproject.com/en/dev/ref/settings/#email-port +EMAIL_PORT = 1025 + + + +# https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config +DEBUG_TOOLBAR_CONFIG = { + "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"], + "SHOW_TEMPLATE_CONTEXT": True, +} +# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips +INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"] +if env("USE_DOCKER") == True: + import socket + + hostname, _, ips = socket.gethostbyname_ex(socket.gethostname()) + INTERNAL_IPS += [".".join(ip.split(".")[:-1] + ["1"]) for ip in ips] + + +INSTALLED_APPS += [ + "django_extensions", + "debug_toolbar", + "whitenoise.runserver_nostatic", + ] # noqa F405 + +MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa F405 + +# Celery +# ------------------------------------------------------------------------------ + +# http://docs.celeryproject.org/en/latest/userguide/configuration.html#task-eager-propagates +CELERY_TASK_EAGER_PROPAGATES = True +# Your stuff... +# ------------------------------------------------------------------------------ diff --git a/config/settings/production.py b/config/settings/production.py new file mode 100644 index 0000000..9c3910f --- /dev/null +++ b/config/settings/production.py @@ -0,0 +1,193 @@ +import logging + +import sentry_sdk +from sentry_sdk.integrations.celery import CeleryIntegration +from sentry_sdk.integrations.django import DjangoIntegration +from sentry_sdk.integrations.logging import LoggingIntegration +from sentry_sdk.integrations.redis import RedisIntegration + +from .base import * # noqa +from .base import env + +# GENERAL +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#secret-key +SECRET_KEY = env("DJANGO_SECRET_KEY") +# https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts +ALLOWED_HOSTS = env.list("DJANGO_ALLOWED_HOSTS", default=["example.com"]) +# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-trusted-origins +CSRF_TRUSTED_ORIGINS = env.list("DJANGO_CSRF_TRUSTED_ORIGINS", default=[]) + +# DATABASES +# ------------------------------------------------------------------------------ +DATABASES["default"] = env.db("DATABASE_URL") # noqa F405 +DATABASES["default"]["ATOMIC_REQUESTS"] = True # noqa F405 +DATABASES["default"]["CONN_MAX_AGE"] = env.int("CONN_MAX_AGE", default=60) # noqa F405 +DATABASES["default"]["ENGINE"] = 'django.db.backends.postgresql' +# CACHES +# ------------------------------------------------------------------------------ +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.redis.RedisCache", + "LOCATION": env("REDIS_URL"), + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + # Mimicing memcache behavior. + # https://github.com/jazzband/django-redis#memcached-exceptions-behavior + "IGNORE_EXCEPTIONS": True, + }, + } +} + +# SECURITY +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") +# https://docs.djangoproject.com/en/dev/ref/settings/#secure-ssl-redirect +SECURE_SSL_REDIRECT = env.bool("DJANGO_SECURE_SSL_REDIRECT", default=True) +# https://docs.djangoproject.com/en/dev/ref/settings/#session-cookie-secure +SESSION_COOKIE_SECURE = env.bool("SESSION_COOKIE_SECURE", default=True) +# https://docs.djangoproject.com/en/dev/ref/settings/#csrf-cookie-secure +CSRF_COOKIE_SECURE = env.bool("CSRF_COOKIE_SECURE", default=True) +# https://docs.djangoproject.com/en/dev/topics/security/#ssl-https +# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-seconds +# TODO: set this to 60 seconds first and then to 518400 once you prove the former works +SECURE_HSTS_SECONDS = 60 +# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-include-subdomains +SECURE_HSTS_INCLUDE_SUBDOMAINS = env.bool( + "DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS", default=True +) +# https://docs.djangoproject.com/en/dev/ref/settings/#secure-hsts-preload +SECURE_HSTS_PRELOAD = env.bool("DJANGO_SECURE_HSTS_PRELOAD", default=True) +# https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff +SECURE_CONTENT_TYPE_NOSNIFF = env.bool( + "DJANGO_SECURE_CONTENT_TYPE_NOSNIFF", default=True +) + +# EMAIL +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#default-from-email +DEFAULT_FROM_EMAIL = env( + "DJANGO_DEFAULT_FROM_EMAIL", + default="SciELO Content Manager ", +) + +# https://docs.djangoproject.com/en/dev/ref/settings/#server-email +SERVER_EMAIL = env("DJANGO_SERVER_EMAIL", default=DEFAULT_FROM_EMAIL) + +# https://docs.djangoproject.com/en/dev/ref/settings/#std-setting-EMAIL_HOST +EMAIL_HOST = env.str("DJANGO_EMAIL_HOST", default="mailrelay.scielo.org") + +# https://docs.djangoproject.com/en/dev/ref/settings/#email-port +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") + +# https://docs.djangoproject.com/en/dev/ref/settings/#std-setting-EMAIL_HOST_PASSWORD +EMAIL_HOST_PASSWORD = env.str("DJANGO_EMAIL_HOST_PASSWORD", default="") + +# https://docs.djangoproject.com/en/dev/ref/settings/#email-use-tls +EMAIL_USE_TLS = env.bool("DJANGO_EMAIL_USE_TLS", default=False) + +# https://docs.djangoproject.com/en/dev/ref/settings/#email-use-ssl +EMAIL_USE_SSL = env.bool("DJANGO_EMAIL_USE_SSL", default=False) +# https://docs.djangoproject.com/en/dev/ref/settings/#email-subject-prefix +EMAIL_SUBJECT_PREFIX = env( + "DJANGO_EMAIL_SUBJECT_PREFIX", + default="[SciELO Content Manager ]", +) + +# Anymail +# ------------------------------------------------------------------------------ +# https://anymail.readthedocs.io/en/stable/installation/#installing-anymail +INSTALLED_APPS += ["anymail"] # noqa F405 +# https://docs.djangoproject.com/en/dev/ref/settings/#email-backend +# https://anymail.readthedocs.io/en/stable/installation/#anymail-settings-reference +# https://anymail.readthedocs.io/en/stable/esps +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +ANYMAIL = {} + +# django-compressor +# ------------------------------------------------------------------------------ +# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_ENABLED +COMPRESS_ENABLED = env.bool("COMPRESS_ENABLED", default=True) +# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_STORAGE +COMPRESS_STORAGE = "compressor.storage.GzipCompressorFileStorage" +# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_URL +COMPRESS_URL = STATIC_URL # noqa F405 +# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_OFFLINE +COMPRESS_OFFLINE = True # Offline compression is required when using Whitenoise +# https://django-compressor.readthedocs.io/en/latest/settings/#django.conf.settings.COMPRESS_FILTERS +COMPRESS_FILTERS = { + "css": [ + "compressor.filters.css_default.CssAbsoluteFilter", + "compressor.filters.cssmin.rCSSMinFilter", + ], + "js": ["compressor.filters.jsmin.JSMinFilter"], +} + +# LOGGING +# ------------------------------------------------------------------------------ +# https://docs.djangoproject.com/en/dev/ref/settings/#logging +# See https://docs.djangoproject.com/en/dev/topics/logging for +# more details on how to customize your logging configuration. + +LOGGING = { + "version": 1, + "disable_existing_loggers": True, + "formatters": { + "verbose": { + "format": "%(levelname)s %(asctime)s %(module)s " + "%(process)d %(thread)d %(message)s" + } + }, + "handlers": { + "console": { + "level": "DEBUG", + "class": "logging.StreamHandler", + "formatter": "verbose", + } + }, + "root": {"level": "INFO", "handlers": ["console"]}, + "loggers": { + "django.db.backends": { + "level": "ERROR", + "handlers": ["console"], + "propagate": False, + }, + # Errors logged by the SDK itself + "sentry_sdk": {"level": "ERROR", "handlers": ["console"], "propagate": False}, + "django.security.DisallowedHost": { + "level": "ERROR", + "handlers": ["console"], + "propagate": False, + }, + }, +} + +# Sentry +# ------------------------------------------------------------------------------ +SENTRY_DSN = env("SENTRY_DSN") +SENTRY_LOG_LEVEL = env.int("DJANGO_SENTRY_LOG_LEVEL", logging.INFO) + +sentry_logging = LoggingIntegration( + level=SENTRY_LOG_LEVEL, # Capture info and above as breadcrumbs + event_level=logging.ERROR, # Send errors as events +) +integrations = [ + sentry_logging, + DjangoIntegration(), + CeleryIntegration(), + RedisIntegration(), +] +sentry_sdk.init( + dsn=SENTRY_DSN, + integrations=integrations, + environment=env("SENTRY_ENVIRONMENT", default="production"), + traces_sample_rate=env.float("SENTRY_TRACES_SAMPLE_RATE", default=1.0), + enable_tracing=True, +) + +# Your stuff... +# ------------------------------------------------------------------------------ diff --git a/config/settings/test.py b/config/settings/test.py new file mode 100644 index 0000000..765f0e0 --- /dev/null +++ b/config/settings/test.py @@ -0,0 +1,30 @@ +import tempfile + +from .base import * + +DEBUG = False +TEMPLATE_DEBUG = False +SECRET_KEY = "test-secret-key-not-for-production" +ALLOWED_HOSTS = ["localhost", "127.0.0.1", "testserver"] +WAGTAILADMIN_BASE_URL = "http://testserver" + +PASSWORD_HASHERS = [ + "django.contrib.auth.hashers.MD5PasswordHasher", +] + +EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" + +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + } +} + +CELERY_TASK_ALWAYS_EAGER = True +CELERY_TASK_EAGER_PROPAGATES = True + +COMPRESS_ENABLED = False + +MEDIA_ROOT = tempfile.mkdtemp(prefix="scielo_tools_test_media_") + +LLAMA_ENABLED = False diff --git a/config/urls.py b/config/urls.py new file mode 100644 index 0000000..41f5ebf --- /dev/null +++ b/config/urls.py @@ -0,0 +1,46 @@ +from django.conf import settings +from django.conf.urls.i18n import i18n_patterns # ← Adicionar esta linha +from django.conf.urls.static import static +from django.contrib import admin +from django.urls import include, path +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView +from wagtail import urls as wagtail_urls +from wagtail.admin import urls as wagtailadmin_urls +from wagtail.documents import urls as wagtaildocs_urls + +from config import api_router as api_router +from core.search import views as search_views +from manuscripts import urls as manuscripts_urls +from manuscripts.views.autocomplete import urlpatterns as autocomplete_admin_urls + +urlpatterns = [ + path("django-admin/", admin.site.urls), + path("admin/", include(wagtailadmin_urls)), + path("documents/", include(wagtaildocs_urls)), + path("search/", search_views.search, name="search"), + # Manuscripts editorial views + path("manuscripts/", include(manuscripts_urls, namespace="manuscripts")), + # JWT + path("api/v1/auth/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), + path( + "api/v1/auth/token/refresh/", TokenRefreshView.as_view(), name="token_refresh" + ), + path("api/v1/", include(api_router)), + # URL para trocar idioma + path("i18n/", include("django.conf.urls.i18n")), +] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + +# URLs com prefixo de idioma +urlpatterns += i18n_patterns( + path("admin/autocomplete/", include(autocomplete_admin_urls)), + # Wagtail pages - deve ser o último + path("", include(wagtail_urls)), + # prefix_default_language=False # Remove /pt-br/ da URL padrão se quiser +) + +if settings.DEBUG: + from django.contrib.staticfiles.urls import staticfiles_urlpatterns + + # Serve static and media files from development server + urlpatterns += staticfiles_urlpatterns() + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/config/wsgi.py b/config/wsgi.py new file mode 100644 index 0000000..58bc24c --- /dev/null +++ b/config/wsgi.py @@ -0,0 +1,21 @@ +""" +WSGI config for scielo_tools project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ +""" + +import os +import sys +from pathlib import Path + +from django.core.wsgi import get_wsgi_application + +ROOT_DIR = Path(__file__).resolve(strict=True).parent.parent +sys.path.append(str(ROOT_DIR / "core")) + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production") + +application = get_wsgi_application() diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/celery_wagtail.py b/core/celery_wagtail.py new file mode 100644 index 0000000..8c373eb --- /dev/null +++ b/core/celery_wagtail.py @@ -0,0 +1,263 @@ +import inspect +import json + +from celery import current_app +from django.conf import settings +from django.contrib import messages +from django.db.models import Case, Value, When +from django.shortcuts import get_object_or_404, redirect +from django.template.defaultfilters import pluralize +from django.urls import path, reverse +from django.utils.translation import gettext_lazy as _ +from django_celery_beat.models import ( + ClockedSchedule, + CrontabSchedule, + IntervalSchedule, + PeriodicTask, + PeriodicTasks, + SolarSchedule, +) +from django_celery_beat.utils import is_database_scheduler +from kombu.utils.json import loads +from wagtail import hooks +from wagtail.admin import messages as wagtail_messages +from wagtail_modeladmin.helpers import ButtonHelper +from wagtail_modeladmin.options import ModelAdmin, ModelAdminGroup, modeladmin_register + +from config.menu import get_menu_order + + +class PeriodicTaskHelper(ButtonHelper): + run_button_classnames = [ + "button-small", + "icon", + ] + + def run_button(self, obj): + text = _("Run") + return { + "url": reverse("celery_task_run") + "?task_id=%s" % str(obj.id), + "label": text, + "classname": self.finalise_classname(self.run_button_classnames), + "title": text, + } + + def get_buttons_for_obj( + self, obj, exclude=None, classnames_add=None, classnames_exclude=None + ): + btns = super().get_buttons_for_obj( + obj, exclude, classnames_add, classnames_exclude + ) + if "run" not in (exclude or []): + btns.append(self.run_button(obj)) + return btns + + +class PeriodicTaskAdmin(ModelAdmin): + button_helper_class = PeriodicTaskHelper + model = PeriodicTask + menu_icon = "cog" + celery_app = current_app + date_hierarchy = "start_time" + list_display = ( + "__str__", + "enabled", + "interval", + "start_time", + "last_run_at", + "one_off", + ) + list_filter = [ + "enabled", + "one_off", + "task", + ] + actions = ("enable_tasks", "disable_tasks", "toggle_tasks", "run_tasks") + search_fields = ("name",) + + def changelist_view(self, request, extra_context=None): + extra_context = extra_context or {} + scheduler = getattr(settings, "CELERYBEAT_SCHEDULER", None) + extra_context["wrong_scheduler"] = not is_database_scheduler(scheduler) + return super().changelist_view(request, extra_context) + + def get_queryset(self, request): + qs = super().get_queryset(request) + return qs.select_related("interval", "crontab", "solar", "clocked") + + def _message_user_about_update(self, request, rows_updated, verb): + self.message_user( + request, + _("{0} task{1} {2} successfully {3}").format( + rows_updated, + pluralize(rows_updated), + pluralize(rows_updated, _("was,were")), + verb, + ), + ) + + def enable_tasks(self, request, queryset): + rows_updated = queryset.update(enabled=True) + PeriodicTasks.update_changed() + self._message_user_about_update(request, rows_updated, "enabled") + + enable_tasks.short_description = _("Enable selected tasks") + + def disable_tasks(self, request, queryset): + rows_updated = queryset.update(enabled=False, last_run_at=None) + PeriodicTasks.update_changed() + self._message_user_about_update(request, rows_updated, "disabled") + + disable_tasks.short_description = _("Disable selected tasks") + + def _toggle_tasks_activity(self, queryset): + return queryset.update( + enabled=Case( + When(enabled=True, then=Value(False)), + default=Value(True), + ) + ) + + def toggle_tasks(self, request, queryset): + rows_updated = self._toggle_tasks_activity(queryset) + PeriodicTasks.update_changed() + self._message_user_about_update(request, rows_updated, "toggled") + + toggle_tasks.short_description = _("Toggle activity of selected tasks") + + def run_tasks(self, request, queryset): + self.celery_app.loader.import_default_modules() + tasks = [ + ( + self.celery_app.tasks.get(task.task), + loads(task.args), + loads(task.kwargs), + task.queue, + task.name, + ) + for task in queryset + ] + + if any(t[0] is None for t in tasks): + for i, t in enumerate(tasks): + if t[0] is None: + break + + not_found_task_name = queryset[i].task + + self.message_user( + request, + _('task "{0}" not found'.format(not_found_task_name)), + level=messages.ERROR, + ) + return + + task_ids = [ + task.apply_async( + args=args, + kwargs=kwargs, + queue=queue, + periodic_task_name=periodic_task_name, + ) + if queue and len(queue) + else task.apply_async( + args=args, kwargs=kwargs, periodic_task_name=periodic_task_name + ) + for task, args, kwargs, queue, periodic_task_name in tasks + ] + tasks_run = len(task_ids) + self.message_user( + request, + _("{0} task{1} {2} successfully run").format( + tasks_run, + pluralize(tasks_run), + pluralize(tasks_run, _("was,were")), + ), + ) + + run_tasks.short_description = _("Run selected tasks") + + +class ClockedScheduleAdmin(ModelAdmin): + menu_icon = "time" + model = ClockedSchedule + fields = ("clocked_time",) + list_display = ("clocked_time",) + + +class IntervalScheduleAdmin(ModelAdmin): + menu_icon = "date" + model = IntervalSchedule + + +class CrontabScheduleAdmin(ModelAdmin): + menu_icon = "date" + model = CrontabSchedule + + +class SolarScheduleAdmin(ModelAdmin): + menu_icon = "date" + model = SolarSchedule + + +class TasksModelsAdminGroup(ModelAdminGroup): + menu_name = "celery_wagtail" + menu_label = _("Tarefas agendadas") + menu_icon = "time" + menu_order = get_menu_order("celery_wagtail") + add_to_admin_menu = False + add_to_settings_menu = True + items = ( + PeriodicTaskAdmin, + CrontabScheduleAdmin, + IntervalScheduleAdmin, + ClockedScheduleAdmin, + SolarScheduleAdmin, + ) + + +modeladmin_register(TasksModelsAdminGroup) + + +def task_run(request): + task_id = int(request.GET.get("task_id", None)) + p_task = get_object_or_404(PeriodicTask, pk=task_id) + + current_app.loader.import_default_modules() + + task = current_app.tasks.get(p_task.task) + if not task: + wagtail_messages.error(request, _('Task "{0}" not found').format(p_task.task)) + return redirect(request.META.get("HTTP_REFERER", "/")) + + kwargs = json.loads(p_task.kwargs) if p_task.kwargs else {} + + try: + sig = inspect.signature(task.run) + has_user_id_param = "user_id" in sig.parameters + has_var_keyword = any( + p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values() + ) + except (ValueError, TypeError): + has_user_id_param = False + has_var_keyword = False + + if (has_user_id_param or has_var_keyword) and getattr(request.user, "id", None) is not None: + kwargs["user_id"] = request.user.id + + task.apply_async( + args=json.loads(p_task.args) if p_task.args else [], + kwargs=kwargs, + queue=p_task.queue, + periodic_task_name=p_task.name, + ) + + wagtail_messages.success(request, _("Task {0} was successfully run").format(p_task.name)) + return redirect(request.META.get("HTTP_REFERER", "/")) + + +@hooks.register("register_admin_urls") +def register_celery_task_url(): + return [ + path("celery_wagtail/", task_run, name="celery_task_run"), + ] diff --git a/core/choices.py b/core/choices.py new file mode 100644 index 0000000..850a6ae --- /dev/null +++ b/core/choices.py @@ -0,0 +1,227 @@ +from django.utils.translation import gettext_lazy as _ + +LANGUAGE = [ + ("aa", "Afar"), + ("af", "Afrikaans"), + ("ak", "Akan"), + ("sq", "Albanian"), + ("am", "Amharic"), + ("ar", "Arabic"), + ("an", "Aragonese"), + ("hy", "Armenian"), + ("as", "Assamese"), + ("av", "Avaric"), + ("ae", "Avestan"), + ("ay", "Aymara"), + ("az", "Azerbaijani"), + ("bm", "Bambara"), + ("ba", "Bashkir"), + ("eu", "Basque"), + ("be", "Belarusian"), + ("bn", "Bengali"), + ("bi", "Bislama"), + ("bs", "Bosnian"), + ("br", "Breton"), + ("bg", "Bulgarian"), + ("my", "Burmese"), + ("ca", "Catalan, Valencian"), + ("ch", "Chamorro"), + ("ce", "Chechen"), + ("ny", "Chichewa, Chewa, Nyanja"), + ("zh", "Chinese"), + ( + "cu", + "Church Slavic, Old Slavonic, Church Slavonic, Old Bulgarian, Old Church Slavonic", + ), + ("cv", "Chuvash"), + ("kw", "Cornish"), + ("co", "Corsican"), + ("cr", "Cree"), + ("hr", "Croatian"), + ("cs", "Czech"), + ("da", "Danish"), + ("dv", "Divehi, Dhivehi, Maldivian"), + ("nl", "Dutch, Flemish"), + ("dz", "Dzongkha"), + ("en", "English"), + ("eo", "Esperanto"), + ("et", "Estonian"), + ("ee", "Ewe"), + ("fo", "Faroese"), + ("fj", "Fijian"), + ("fi", "Finnish"), + ("fr", "French"), + ("fy", "Western Frisian"), + ("ff", "Fulah"), + ("gd", "Gaelic, Scottish Gaelic"), + ("gl", "Galician"), + ("lg", "Ganda"), + ("ka", "Georgian"), + ("de", "German"), + ("el", "Greek, Modern (1453–)"), + ("kl", "Kalaallisut, Greenlandic"), + ("gn", "Guarani"), + ("gu", "Gujarati"), + ("ht", "Haitian, Haitian Creole"), + ("ha", "Hausa"), + ("he", "Hebrew"), + ("hz", "Herero"), + ("hi", "Hindi"), + ("ho", "Hiri Motu"), + ("hu", "Hungarian"), + ("is", "Icelandic"), + ("io", "Ido"), + ("ig", "Igbo"), + ("id", "Indonesian"), + ("ia", "Interlingua (International Auxiliary Language Association)"), + ("ie", "Interlingue, Occidental"), + ("iu", "Inuktitut"), + ("ik", "Inupiaq"), + ("ga", "Irish"), + ("it", "Italian"), + ("ja", "Japanese"), + ("jv", "Javanese"), + ("kn", "Kannada"), + ("kr", "Kanuri"), + ("ks", "Kashmiri"), + ("kk", "Kazakh"), + ("km", "Central Khmer"), + ("ki", "Kikuyu, Gikuyu"), + ("rw", "Kinyarwanda"), + ("ky", "Kirghiz, Kyrgyz"), + ("kv", "Komi"), + ("kg", "Kongo"), + ("ko", "Korean"), + ("kj", "Kuanyama, Kwanyama"), + ("ku", "Kurdish"), + ("lo", "Lao"), + ("la", "Latin"), + ("lv", "Latvian"), + ("li", "Limburgan, Limburger, Limburgish"), + ("ln", "Lingala"), + ("lt", "Lithuanian"), + ("lu", "Luba-Katanga"), + ("lb", "Luxembourgish, Letzeburgesch"), + ("mk", "Macedonian"), + ("mg", "Malagasy"), + ("ms", "Malay"), + ("ml", "Malayalam"), + ("mt", "Maltese"), + ("gv", "Manx"), + ("mi", "Maori"), + ("mr", "Marathi"), + ("mh", "Marshallese"), + ("mn", "Mongolian"), + ("na", "Nauru"), + ("nv", "Navajo, Navaho"), + ("nd", "North Ndebele"), + ("nr", "South Ndebele"), + ("ng", "Ndonga"), + ("ne", "Nepali"), + ("no", "Norwegian"), + ("nb", "Norwegian Bokmål"), + ("nn", "Norwegian Nynorsk"), + ("ii", "Sichuan Yi, Nuosu"), + ("oc", "Occitan"), + ("oj", "Ojibwa"), + ("or", "Oriya"), + ("om", "Oromo"), + ("os", "Ossetian, Ossetic"), + ("pi", "Pali"), + ("ps", "Pashto, Pushto"), + ("fa", "Persian"), + ("pl", "Polish"), + ("pt", "Português"), + ("pa", "Punjabi, Panjabi"), + ("qu", "Quechua"), + ("ro", "Romanian, Moldavian, Moldovan"), + ("rm", "Romansh"), + ("rn", "Rundi"), + ("ru", "Russian"), + ("se", "Northern Sami"), + ("sm", "Samoan"), + ("sg", "Sango"), + ("sa", "Sanskrit"), + ("sc", "Sardinian"), + ("sr", "Serbian"), + ("sn", "Shona"), + ("sd", "Sindhi"), + ("si", "Sinhala, Sinhalese"), + ("sk", "Slovak"), + ("sl", "Slovenian"), + ("so", "Somali"), + ("st", "Southern Sotho"), + ("es", "Español"), + ("su", "Sundanese"), + ("sw", "Swahili"), + ("ss", "Swati"), + ("sv", "Swedish"), + ("tl", "Tagalog"), + ("ty", "Tahitian"), + ("tg", "Tajik"), + ("ta", "Tamil"), + ("tt", "Tatar"), + ("te", "Telugu"), + ("th", "Thai"), + ("bo", "Tibetan"), + ("ti", "Tigrinya"), + ("to", "Tonga (Tonga Islands)"), + ("ts", "Tsonga"), + ("tn", "Tswana"), + ("tr", "Turkish"), + ("tk", "Turkmen"), + ("tw", "Twi"), + ("ug", "Uighur, Uyghur"), + ("uk", "Ukrainian"), + ("ur", "Urdu"), + ("uz", "Uzbek"), + ("ve", "Venda"), + ("vi", "Vietnamese"), + ("vo", "Volapük"), + ("wa", "Walloon"), + ("cy", "Welsh"), + ("wo", "Wolof"), + ("xh", "Xhosa"), + ("yi", "Yiddish"), + ("yo", "Yoruba"), + ("za", "Zhuang, Chuang"), + ("zu", "Zulu"), +] + +ROLE = [ + ("Editor-in-Chief", _("Editor-in-Chief")), + ("Executive Editor(s)", _("Executive Editor(s)")), + ("Associate or Section Editor(s)", _("Associate or Section Editor(s)")), + ("Technical Team", _("Technical Team")), +] + +MONTHS = [ + ("01", _("January")), + ("02", _("February")), + ("03", _("March")), + ("04", _("April")), + ("05", _("May")), + ("06", _("June")), + ("07", _("July")), + ("08", _("August")), + ("09", _("September")), + ("10", _("October")), + ("11", _("November")), + ("12", _("December")), +] + +# https://creativecommons.org/share-your-work/cclicenses/ +# There are six different license types, listed from most to least permissive here: +LICENSE_TYPES = [ + ("by", _("by")), + ("by-sa", _("by-sa")), + ("by-nc", _("by-nc")), + ("by-nc-sa", _("by-nc-sa")), + ("by-nd", _("by-nd")), + ("by-nc-nd", _("by-nc-nd")), +] + +GENDER_CHOICES = [ + ('M', _('Male')), + ('F', _('Female')), +] \ No newline at end of file diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..913bfbb --- /dev/null +++ b/core/forms.py @@ -0,0 +1,15 @@ +from wagtail.admin.forms import WagtailAdminModelForm + + +class CoreAdminModelForm(WagtailAdminModelForm): + def save_all(self, user): + model_with_creator = super().save(commit=False) + + if self.instance.pk is not None: + model_with_creator.updated_by = user + else: + model_with_creator.creator = user + + self.save() + + return model_with_creator \ No newline at end of file diff --git a/core/home/__init__.py b/core/home/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/home/migrations/0001_initial.py b/core/home/migrations/0001_initial.py new file mode 100644 index 0000000..ab7f8ad --- /dev/null +++ b/core/home/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 6.0.5 on 2026-06-09 00:35 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('wagtailcore', '0097_baselogentry_uuid_action_timestamp_indexes'), + ] + + operations = [ + migrations.CreateModel( + name='HomePage', + fields=[ + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')), + ], + options={ + 'abstract': False, + }, + bases=('wagtailcore.page',), + ), + ] diff --git a/core/home/migrations/__init__.py b/core/home/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/home/models.py b/core/home/models.py new file mode 100644 index 0000000..f24c860 --- /dev/null +++ b/core/home/models.py @@ -0,0 +1,5 @@ +from wagtail.models import Page + + +class HomePage(Page): + pass diff --git a/core/home/static/css/welcome_page.css b/core/home/static/css/welcome_page.css new file mode 100644 index 0000000..bad2933 --- /dev/null +++ b/core/home/static/css/welcome_page.css @@ -0,0 +1,184 @@ +html { + box-sizing: border-box; +} + +*, +*:before, +*:after { + box-sizing: inherit; +} + +body { + max-width: 960px; + min-height: 100vh; + margin: 0 auto; + padding: 0 15px; + color: #231f20; + font-family: 'Helvetica Neue', 'Segoe UI', Arial, sans-serif; + line-height: 1.25; +} + +a { + background-color: transparent; + color: #308282; + text-decoration: underline; +} + +a:hover { + color: #ea1b10; +} + +h1, +h2, +h3, +h4, +h5, +p, +ul { + padding: 0; + margin: 0; + font-weight: 400; +} + +svg:not(:root) { + overflow: hidden; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 20px; + padding-bottom: 10px; + border-bottom: 1px solid #e6e6e6; +} + +.logo { + width: 150px; + margin-inline-end: 20px; +} + +.logo a { + display: block; +} + +.figure-logo { + max-width: 150px; + max-height: 55.1px; +} + +.release-notes { + font-size: 14px; +} + +.main { + padding: 40px 0; + margin: 0 auto; + text-align: center; +} + +.figure-space { + max-width: 265px; +} + +@keyframes pos { + 0%, 100% { + transform: rotate(-6deg); + } + 50% { + transform: rotate(6deg); + } +} + +.egg { + fill: #43b1b0; + animation: pos 3s ease infinite; + transform: translateY(50px); + transform-origin: 50% 80%; +} + +.main-text { + max-width: 400px; + margin: 5px auto; +} + +.main-text h1 { + font-size: 22px; +} + +.main-text p { + margin: 15px auto 0; +} + +.footer { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + border-top: 1px solid #e6e6e6; + padding: 10px; +} + +.option { + display: block; + padding: 10px 10px 10px 34px; + position: relative; + text-decoration: none; +} + +.option svg { + width: 24px; + height: 24px; + fill: gray; + border: 1px solid #d9d9d9; + padding: 5px; + border-radius: 100%; + top: 10px; + inset-inline-start: 0; + position: absolute; +} + +.option h2 { + font-size: 19px; + text-decoration: underline; +} + +.option p { + padding-top: 3px; + color: #231f20; + font-size: 15px; + font-weight: 300; +} + +@media (max-width: 996px) { + body { + max-width: 780px; + } +} + +@media (max-width: 767px) { + .option { + flex: 0 0 50%; + } +} + +@media (max-width: 599px) { + .main { + padding: 20px 0; + } + + .figure-space { + max-width: 200px; + } + + .footer { + display: block; + width: 300px; + margin: 0 auto; + } +} + +@media (max-width: 360px) { + .header-link { + max-width: 100px; + } +} diff --git a/core/home/templates/home/home_page.html b/core/home/templates/home/home_page.html new file mode 100644 index 0000000..db9e9b0 --- /dev/null +++ b/core/home/templates/home/home_page.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} +{% load static %} + +{% block body_class %}template-homepage{% endblock %} + +{% block extra_css %} + +{% comment %} +Delete the line below if you're just getting started and want to remove the welcome screen! +{% endcomment %} + +{% endblock extra_css %} + +{% block content %} + +{% comment %} +Delete the line below if you're just getting started and want to remove the welcome screen! +{% endcomment %} +{% include 'home/welcome_page.html' %} + +{% endblock content %} diff --git a/core/home/templates/home/welcome_page.html b/core/home/templates/home/welcome_page.html new file mode 100644 index 0000000..dcacaf3 --- /dev/null +++ b/core/home/templates/home/welcome_page.html @@ -0,0 +1,52 @@ +{% load i18n wagtailcore_tags %} + +
+ + +
+
+
+ +
+
+

{% trans "Welcome to your new Wagtail site!" %}

+

{% trans 'Please feel free to join our community on Slack, or get started with one of the links below.' %}

+
+
+ diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..35440d1 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,88 @@ +# Generated by Django 6.0.5 on 2026-06-09 00:35 + +import wagtail.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='CoreSyncState', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('resource', models.CharField(max_length=50, unique=True, verbose_name='Resource')), + ('last_updated_at', models.DateTimeField(blank=True, null=True, verbose_name='Last updated at')), + ('last_success_at', models.DateTimeField(blank=True, null=True, verbose_name='Last success at')), + ], + options={ + 'verbose_name': 'Core sync state', + 'verbose_name_plural': 'Core sync states', + }, + ), + migrations.CreateModel( + name='FlexibleDate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('year', models.IntegerField(blank=True, null=True, verbose_name='Year')), + ('month', models.IntegerField(blank=True, null=True, verbose_name='Month')), + ('day', models.IntegerField(blank=True, null=True, verbose_name='Day')), + ], + ), + migrations.CreateModel( + name='Gender', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Last update date')), + ('code', models.CharField(blank=True, max_length=5, null=True, verbose_name='Code')), + ('gender', models.CharField(blank=True, max_length=50, null=True, verbose_name='Sex')), + ], + ), + migrations.CreateModel( + name='Language', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Last update date')), + ('name', models.TextField(blank=True, null=True, verbose_name='Language Name')), + ('code2', models.TextField(blank=True, null=True, verbose_name='Language code 2')), + ], + options={ + 'verbose_name': 'Language', + 'verbose_name_plural': 'Languages', + }, + ), + migrations.CreateModel( + name='License', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Last update date')), + ('license_type', models.CharField(blank=True, max_length=255, null=True)), + ], + options={ + 'verbose_name': 'License', + 'verbose_name_plural': 'Licenses', + }, + ), + migrations.CreateModel( + name='LicenseStatement', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Last update date')), + ('url', models.CharField(blank=True, max_length=255, null=True)), + ('license_p', wagtail.fields.RichTextField(blank=True, null=True)), + ], + options={ + 'verbose_name': 'License', + 'verbose_name_plural': 'Licenses', + }, + ), + ] diff --git a/core/migrations/0002_initial.py b/core/migrations/0002_initial.py new file mode 100644 index 0000000..03522c2 --- /dev/null +++ b/core/migrations/0002_initial.py @@ -0,0 +1,88 @@ +# Generated by Django 6.0.5 on 2026-06-09 00:35 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('core', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='gender', + name='creator', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_creator', to=settings.AUTH_USER_MODEL, verbose_name='Creator'), + ), + migrations.AddField( + model_name='gender', + name='updated_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_last_mod_user', to=settings.AUTH_USER_MODEL, verbose_name='Updater'), + ), + migrations.AddField( + model_name='language', + name='creator', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_creator', to=settings.AUTH_USER_MODEL, verbose_name='Creator'), + ), + migrations.AddField( + model_name='language', + name='updated_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_last_mod_user', to=settings.AUTH_USER_MODEL, verbose_name='Updater'), + ), + migrations.AddField( + model_name='license', + name='creator', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_creator', to=settings.AUTH_USER_MODEL, verbose_name='Creator'), + ), + migrations.AddField( + model_name='license', + name='updated_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_last_mod_user', to=settings.AUTH_USER_MODEL, verbose_name='Updater'), + ), + migrations.AddField( + model_name='licensestatement', + name='creator', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_creator', to=settings.AUTH_USER_MODEL, verbose_name='Creator'), + ), + migrations.AddField( + model_name='licensestatement', + name='language', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.language'), + ), + migrations.AddField( + model_name='licensestatement', + name='license', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='core.license'), + ), + migrations.AddField( + model_name='licensestatement', + name='updated_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_last_mod_user', to=settings.AUTH_USER_MODEL, verbose_name='Updater'), + ), + migrations.AlterUniqueTogether( + name='gender', + unique_together={('code', 'gender')}, + ), + migrations.AddIndex( + model_name='license', + index=models.Index(fields=['license_type'], name='core_licens_license_5d1905_idx'), + ), + migrations.AlterUniqueTogether( + name='license', + unique_together={('license_type',)}, + ), + migrations.AddIndex( + model_name='licensestatement', + index=models.Index(fields=['url'], name='core_licens_url_ec8078_idx'), + ), + migrations.AlterUniqueTogether( + name='licensestatement', + unique_together={('url', 'license_p', 'language')}, + ), + ] diff --git a/core/migrations/__init__.py b/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/models.py b/core/models.py new file mode 100644 index 0000000..c376b35 --- /dev/null +++ b/core/models.py @@ -0,0 +1,590 @@ +import os + +from django.contrib.auth import get_user_model +from django.db import IntegrityError, models +from django.db.models import Case, IntegerField, Value, When +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ +from wagtail.admin.panels import FieldPanel +from wagtail.fields import RichTextField +from wagtailautocomplete.edit_handlers import AutocompletePanel + +from . import choices +from .utils.requester import language_iso + +User = get_user_model() + + +class CommonControlField(models.Model): + """ + Class with common control fields. + + Fields: + created: Date time when the record was created + updated: Date time with the last update date + creator: The creator of the record + updated_by: Store the last updator of the record + """ + + # Creation date + created = models.DateTimeField(verbose_name=_("Creation date"), auto_now_add=True) + + # Update date + updated = models.DateTimeField(verbose_name=_("Last update date"), auto_now=True) + + # Creator user + creator = models.ForeignKey( + User, + verbose_name=_("Creator"), + related_name="%(class)s_creator", + editable=False, + on_delete=models.SET_NULL, + null=True, + ) + + # Last modifier user + updated_by = models.ForeignKey( + User, + verbose_name=_("Updater"), + related_name="%(class)s_last_mod_user", + editable=False, + null=True, + blank=True, + on_delete=models.SET_NULL, + ) + + class Meta: + abstract = True + + +class Gender(CommonControlField): + """ + Class of gender + + Fields: + sex: physical state of being either male, female, or intersex + """ + + code = models.CharField(_("Code"), max_length=5, null=True, blank=True) + + gender = models.CharField(_("Sex"), max_length=50, null=True, blank=True) + + autocomplete_search_filter = "code" + + def autocomplete_label(self): + return str(self) + + panels = [ + FieldPanel("code"), + FieldPanel("gender"), + ] + + class Meta: + unique_together = [("code", "gender")] + + def __unicode__(self): + return self.gender or self.code + + def __str__(self): + return self.gender or self.code + + @classmethod + def load(cls, user): + for item in choices.GENDER_CHOICES: + code, value = item + cls.create_or_update(user, code=code, gender=value) + + @classmethod + def _get(cls, code=None, gender=None): + try: + return cls.objects.get(code=code, gender=gender) + except cls.MultipleObjectsReturned: + return cls.objects.filter(code=code, gender=gender).first() + + @classmethod + def _create(cls, user, code=None, gender=None): + try: + obj = cls() + obj.gender = gender + obj.code = code + obj.creator = user + obj.save() + return obj + except IntegrityError: + return cls._get(code, gender) + + @classmethod + def create_or_update(cls, user, code, gender=None): + try: + return cls._get(code, gender) + except cls.DoesNotExist: + return cls._create(user, code, gender) + + +class Language(CommonControlField): + """ + Represent the list of states + + Fields: + name + code2 + """ + + name = models.TextField(_("Language Name"), blank=True, null=True) + code2 = models.TextField(_("Language code 2"), blank=True, null=True) + + autocomplete_search_field = "name" + + def autocomplete_label(self): + return str(self) + + class Meta: + verbose_name = _("Language") + verbose_name_plural = _("Languages") + + def __unicode__(self): + if self.name or self.code2: + return f"{self.name} | {self.code2}" + return "None" + + def __str__(self): + if self.name or self.code2: + return f"{self.name} | {self.code2}" + return "None" + + @classmethod + def load(cls, user): + if cls.objects.count() == 0: + for k, v in choices.LANGUAGE: + cls.get_or_create(name=v, code2=k, creator=user) + + @classmethod + def get_or_create(cls, name=None, code2=None, creator=None): + code2 = language_iso(code2) + if code2: + try: + return cls.objects.get(code2=code2) + except cls.DoesNotExist: + pass + + if name: + try: + return cls.objects.get(name=name) + except cls.DoesNotExist: + pass + + if name or code2: + obj = Language() + obj.name = name + obj.code2 = code2 or "" + obj.creator = creator + obj.save() + return obj + + +class TextWithLang(models.Model): + text = models.TextField(_("Text"), null=True, blank=True) + language = models.ForeignKey( + Language, + on_delete=models.SET_NULL, + verbose_name=_("Language"), + null=True, + blank=True, + ) + + panels = [FieldPanel("text"), AutocompletePanel("language")] + + class Meta: + abstract = True + + +class TextLanguageMixin(models.Model): + rich_text = RichTextField(_("Rich Text"), null=True, blank=True) + plain_text = models.TextField(_("Plain Text"), null=True, blank=True) + language = models.ForeignKey( + Language, + on_delete=models.SET_NULL, + verbose_name=_("Language"), + null=True, + blank=True, + ) + + panels = [ + AutocompletePanel("language"), + FieldPanel("rich_text"), + FieldPanel("plain_text"), + ] + + class Meta: + abstract = True + + +class LanguageFallbackManager(models.Manager): + def get_object_in_preferred_language(self, language): + mission = self.filter(language=language) + if mission: + return mission + + language_order = ["pt", "es", "en"] + langs = self.all().values_list("language", flat=True) + languages = Language.objects.filter(id__in=langs) + + # Define a ordem baseado na lista language_order + order = [ + When(code2=lang, then=Value(i)) for i, lang in enumerate(language_order) + ] + ordered_languages = languages.annotate( + language_order=Case( + *order, default=Value(len(language_order)), output_field=IntegerField() + ) + ).order_by("language_order") + + for lang in ordered_languages: + mission = self.filter(language=lang) + if mission: + return mission + return None + + +class RichTextWithLanguage(models.Model): + rich_text = RichTextField(_("Rich Text"), null=True, blank=True) + language = models.ForeignKey( + Language, + on_delete=models.SET_NULL, + verbose_name=_("Language"), + null=True, + blank=True, + ) + + panels = [ + AutocompletePanel("language"), + FieldPanel("rich_text"), + ] + + objects = LanguageFallbackManager() + + class Meta: + abstract = True + + +class FlexibleDate(models.Model): + year = models.IntegerField(_("Year"), null=True, blank=True) + month = models.IntegerField(_("Month"), null=True, blank=True) + day = models.IntegerField(_("Day"), null=True, blank=True) + + def __unicode__(self): + return "%s/%s/%s" % (self.year, self.month, self.day) + + def __str__(self): + return "%s/%s/%s" % (self.year, self.month, self.day) + + @property + def data(self): + return dict( + date__year=self.year, + date__month=self.month, + date__day=self.day, + ) + + +class License(CommonControlField): + license_type = models.CharField(max_length=255, null=True, blank=True) + + autocomplete_search_field = "license_type" + + def autocomplete_label(self): + return str(self) + + panels = [ + FieldPanel("license_type"), + ] + + class Meta: + unique_together = [("license_type",)] + verbose_name = _("License") + verbose_name_plural = _("Licenses") + indexes = [ + models.Index( + fields=[ + "license_type", + ] + ), + ] + + def __unicode__(self): + return self.license_type or "" + + def __str__(self): + return self.license_type or "" + + @classmethod + def load(cls, user): + for license_type, v in choices.LICENSE_TYPES: + cls.create_or_update(user, license_type) + + @classmethod + def get( + cls, + license_type, + ): + if not license_type: + raise ValueError("License.get requires license_type parameters") + filters = dict(license_type__iexact=license_type) + try: + return cls.objects.get(**filters) + except cls.MultipleObjectsReturned: + return cls.objects.filter(**filters).first() + + @classmethod + def create( + cls, + user, + license_type=None, + ): + try: + obj = cls() + obj.creator = user + obj.license_type = license_type or obj.license_type + obj.save() + return obj + except IntegrityError: + return cls.get(license_type=license_type) + + @classmethod + def create_or_update( + cls, + user, + license_type=None, + ): + try: + return cls.get(license_type=license_type) + except cls.DoesNotExist: + return cls.create(user, license_type) + + +class LicenseStatement(CommonControlField): + url = models.CharField(max_length=255, null=True, blank=True) + license_p = RichTextField(null=True, blank=True) + language = models.ForeignKey( + Language, on_delete=models.SET_NULL, null=True, blank=True + ) + license = models.ForeignKey( + License, on_delete=models.SET_NULL, null=True, blank=True + ) + + panels = [ + FieldPanel("url"), + FieldPanel("license_p"), + AutocompletePanel("language"), + AutocompletePanel("license"), + ] + + class Meta: + unique_together = [("url", "license_p", "language")] + verbose_name = _("License") + verbose_name_plural = _("Licenses") + indexes = [ + models.Index( + fields=[ + "url", + ] + ), + ] + + def __unicode__(self): + return self.url or "" + + def __str__(self): + return self.url or "" + + @classmethod + def get( + cls, + url=None, + license_p=None, + language=None, + ): + if not url and not license_p: + raise ValueError("LicenseStatement.get requires url or license_p") + try: + return cls.objects.get( + url__iexact=url, license_p__iexact=license_p, language=language + ) + except cls.MultipleObjectsReturned: + return cls.objects.filter( + url__iexact=url, license_p__iexact=license_p, language=language + ).first() + + @classmethod + def create( + cls, + user, + url=None, + license_p=None, + language=None, + license=None, + ): + if not url and not license_p: + raise ValueError("LicenseStatement.create requires url or license_p") + try: + obj = cls() + obj.creator = user + obj.url = url or obj.url + obj.license_p = license_p or obj.license_p + obj.language = language or obj.language + # instance of License + obj.license = license or obj.license + obj.save() + return obj + except IntegrityError: + return cls.get(url, license_p, language) + + @classmethod + def create_or_update( + cls, + user, + url=None, + license_p=None, + language=None, + license=None, + ): + try: + data = dict( + url=url, license_p=license_p, language=language and language.code2 + ) + try: + obj = cls.get(url, license_p, language) + obj.updated_by = user + obj.url = url or obj.url + obj.license_p = license_p or obj.license_p + obj.language = language or obj.language + # instance of License + obj.license = license or obj.license + obj.save() + return obj + except cls.DoesNotExist: + return cls.create(user, url, license_p, language, license) + except Exception as e: + raise ValueError( + f"Unable to create or update LicenseStatement for {data}: {type(e)} {e}" + ) + + @staticmethod + def parse_url(url): + license_type = None + license_version = None + license_language = None + + url = url.lower() + url_parts = url.split("/") + if not url_parts: + return {} + + license_types = dict(choices.LICENSE_TYPES) + for lic_type in license_types.keys(): + if lic_type in url_parts: + license_type = lic_type + + try: + version = url.split(f"/{license_type}/") + version = version[-1].split("/")[0] + isdigit = False + for c in version.split("."): + if c.isdigit(): + isdigit = True + continue + else: + isdigit = False + break + if isdigit: + license_version = version + except (AttributeError, TypeError, ValueError): + pass + break + + return dict( + license_type=license_type, + license_version=license_version, + license_language=license_language, + ) + + +class FileWithLang(models.Model): + file = models.ForeignKey( + "wagtaildocs.Document", + null=True, + blank=True, + on_delete=models.SET_NULL, + verbose_name=_("File"), + help_text="", + related_name="+", + ) + + language = models.ForeignKey( + Language, + on_delete=models.SET_NULL, + verbose_name=_("Language"), + null=True, + blank=True, + ) + + panels = [ + AutocompletePanel("language"), + FieldPanel("file"), + ] + + @property + def filename(self): + return os.path.basename(self.file.name) + + class Meta: + abstract = True + + +class CoreSyncState(models.Model): + """ + Guarda o checkpoint da última coleta da API Core por recurso. + + A próxima coleta deve sempre retomar a partir de ``last_updated_at``. + """ + + resource = models.CharField(_("Resource"), max_length=50, unique=True) + last_updated_at = models.DateTimeField(_("Last updated at"), null=True, blank=True) + last_success_at = models.DateTimeField(_("Last success at"), null=True, blank=True) + + class Meta: + verbose_name = _("Core sync state") + verbose_name_plural = _("Core sync states") + + def __unicode__(self): + return self.resource + + def __str__(self): + return self.resource + + @classmethod + def get_for_resource(cls, resource): + obj, _ = cls.objects.get_or_create(resource=resource) + return obj + + def get_from_date_updated(self, default): + """ + Retorna a data inicial para o filtro ``from_date_created`` da API. + + Usa sempre a última data coletada; se ainda não houver checkpoint, + retorna ``default``. + """ + if self.last_updated_at: + return self.last_updated_at.date().isoformat() + return default + + def update_checkpoint(self, max_updated_at=None): + """ + Atualiza o checkpoint após uma execução bem-sucedida de sync. + """ + update_fields = ["last_success_at"] + if max_updated_at: + self.last_updated_at = max_updated_at + update_fields.append("last_updated_at") + self.last_success_at = timezone.now() + self.save(update_fields=update_fields) diff --git a/core/search/__init__.py b/core/search/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/search/templates/search/search.html b/core/search/templates/search/search.html new file mode 100644 index 0000000..476427f --- /dev/null +++ b/core/search/templates/search/search.html @@ -0,0 +1,38 @@ +{% extends "base.html" %} +{% load static wagtailcore_tags %} + +{% block body_class %}template-searchresults{% endblock %} + +{% block title %}Search{% endblock %} + +{% block content %} +

Search

+ +
+ + +
+ +{% if search_results %} +
    + {% for result in search_results %} +
  • +

    {{ result }}

    + {% if result.search_description %} + {{ result.search_description }} + {% endif %} +
  • + {% endfor %} +
+ +{% if search_results.has_previous %} +Previous +{% endif %} + +{% if search_results.has_next %} +Next +{% endif %} +{% elif search_query %} +No results found +{% endif %} +{% endblock %} diff --git a/core/search/views.py b/core/search/views.py new file mode 100644 index 0000000..21bd31d --- /dev/null +++ b/core/search/views.py @@ -0,0 +1,38 @@ +from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator +from django.template.response import TemplateResponse +from wagtail.models import Page + + +def search(request): + search_query = request.GET.get("query", None) + page = request.GET.get("page", 1) + + # Search + if search_query: + search_results = Page.objects.live().search(search_query) + + # To log this query for use with the "Promoted search results" module: + + # query = Query.get(search_query) + # query.add_hit() + + else: + search_results = Page.objects.none() + + # Pagination + paginator = Paginator(search_results, 10) + try: + search_results = paginator.page(page) + except PageNotAnInteger: + search_results = paginator.page(1) + except EmptyPage: + search_results = paginator.page(paginator.num_pages) + + return TemplateResponse( + request, + "search/search.html", + { + "search_query": search_query, + "search_results": search_results, + }, + ) diff --git a/core/static/css/scielo_tools.css b/core/static/css/scielo_tools.css new file mode 100644 index 0000000..e69de29 diff --git a/core/static/img/favicons/android-chrome-192x192.png b/core/static/img/favicons/android-chrome-192x192.png new file mode 100755 index 0000000..5f2a893 Binary files /dev/null and b/core/static/img/favicons/android-chrome-192x192.png differ diff --git a/core/static/img/favicons/android-chrome-512x512 2.png b/core/static/img/favicons/android-chrome-512x512 2.png new file mode 100755 index 0000000..e69de29 diff --git a/core/static/img/favicons/android-chrome-512x512.png b/core/static/img/favicons/android-chrome-512x512.png new file mode 100755 index 0000000..bc1ad30 Binary files /dev/null and b/core/static/img/favicons/android-chrome-512x512.png differ diff --git a/core/static/img/favicons/apple-touch-icon.png b/core/static/img/favicons/apple-touch-icon.png new file mode 100755 index 0000000..68ee7dd Binary files /dev/null and b/core/static/img/favicons/apple-touch-icon.png differ diff --git a/core/static/img/favicons/favicon-16x16.png b/core/static/img/favicons/favicon-16x16.png new file mode 100755 index 0000000..9de9a53 Binary files /dev/null and b/core/static/img/favicons/favicon-16x16.png differ diff --git a/core/static/img/favicons/favicon-32x32.png b/core/static/img/favicons/favicon-32x32.png new file mode 100755 index 0000000..ba5ee44 Binary files /dev/null and b/core/static/img/favicons/favicon-32x32.png differ diff --git a/core/static/img/favicons/favicon.ico b/core/static/img/favicons/favicon.ico new file mode 100755 index 0000000..f62cda5 Binary files /dev/null and b/core/static/img/favicons/favicon.ico differ diff --git a/core/static/img/favicons/site.webmanifest b/core/static/img/favicons/site.webmanifest new file mode 100755 index 0000000..45dc8a2 --- /dev/null +++ b/core/static/img/favicons/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/core/static/img/logo-scielo-min.jpg b/core/static/img/logo-scielo-min.jpg new file mode 100644 index 0000000..867c624 Binary files /dev/null and b/core/static/img/logo-scielo-min.jpg differ diff --git a/core/static/img/logo-scielo-no-label-negative.svg b/core/static/img/logo-scielo-no-label-negative.svg new file mode 100644 index 0000000..7b372f6 --- /dev/null +++ b/core/static/img/logo-scielo-no-label-negative.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/core/static/img/logo-scielo-no-label.svg b/core/static/img/logo-scielo-no-label.svg new file mode 100644 index 0000000..f208ac8 --- /dev/null +++ b/core/static/img/logo-scielo-no-label.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/static/img/logo-scielo-signature.png b/core/static/img/logo-scielo-signature.png new file mode 100644 index 0000000..5f5e840 Binary files /dev/null and b/core/static/img/logo-scielo-signature.png differ diff --git a/core/static/img/logo-scielo-svg.svg b/core/static/img/logo-scielo-svg.svg new file mode 100644 index 0000000..f84dd49 --- /dev/null +++ b/core/static/img/logo-scielo-svg.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/static/img/logo-scielo.svg b/core/static/img/logo-scielo.svg new file mode 100644 index 0000000..d690f93 --- /dev/null +++ b/core/static/img/logo-scielo.svg @@ -0,0 +1,2638 @@ + + + diff --git a/core/static/js/scielo_tools.js b/core/static/js/scielo_tools.js new file mode 100644 index 0000000..e69de29 diff --git a/core/templates/404.html b/core/templates/404.html new file mode 100644 index 0000000..f19ab95 --- /dev/null +++ b/core/templates/404.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} + +{% block title %}Page not found{% endblock %} + +{% block body_class %}template-404{% endblock %} + +{% block content %} +

Page not found

+ +

Sorry, this page could not be found.

+{% endblock %} diff --git a/core/templates/500.html b/core/templates/500.html new file mode 100644 index 0000000..77379e5 --- /dev/null +++ b/core/templates/500.html @@ -0,0 +1,13 @@ + + + + + Internal server error + + + +

Internal server error

+ +

Sorry, there seems to be an error. Please try again soon.

+ + diff --git a/core/templates/admin/djcelery/change_list.html b/core/templates/admin/djcelery/change_list.html new file mode 100644 index 0000000..20b269f --- /dev/null +++ b/core/templates/admin/djcelery/change_list.html @@ -0,0 +1,20 @@ +{% extends "admin/change_list.html" %} +{% load i18n %} + +{% block breadcrumbs %} + + {% if wrong_scheduler %} +
    +
  • + Periodic tasks won't be dispatched unless you set the + CELERYBEAT_SCHEDULER setting to + djcelery.schedulers.DatabaseScheduler, + or specify it using the -S option to celerybeat +
  • +
+ {% endif %} +{% endblock %} diff --git a/core/templates/base.html b/core/templates/base.html new file mode 100644 index 0000000..4e45d96 --- /dev/null +++ b/core/templates/base.html @@ -0,0 +1,46 @@ +{% load static wagtailcore_tags wagtailuserbar %} + + + + + + + {% block title %} + {% if page.seo_title %}{{ page.seo_title }}{% else %}{{ page.title }}{% endif %} + {% endblock %} + {% block title_suffix %} + {% wagtail_site as current_site %} + {% if current_site and current_site.site_name %}- {{ current_site.site_name }}{% endif %} + {% endblock %} + + {% if page.search_description %} + + {% endif %} + + + {# Force all links in the live preview panel to be opened in a new tab #} + {% if request.in_preview_panel %} + + {% endif %} + + {# Global stylesheets #} + + + {% block extra_css %} + {# Override this in templates to add extra stylesheets #} + {% endblock %} + + + + {% wagtailuserbar %} + + {% block content %}{% endblock %} + + {# Global javascript #} + + + {% block extra_js %} + {# Override this in templates to add extra javascript #} + {% endblock %} + + diff --git a/core/utils/__init__.py b/core/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/utils/requester.py b/core/utils/requester.py new file mode 100644 index 0000000..cb0c045 --- /dev/null +++ b/core/utils/requester.py @@ -0,0 +1,184 @@ +import logging +import re + +import requests +from django.contrib.auth import get_user_model +from langcodes import standardize_tag, tag_is_valid +from tenacity import ( + retry, + retry_if_exception_type, + stop_after_attempt, + wait_exponential, +) + +logger = logging.getLogger(__name__) +User = get_user_model() + + +def language_iso(code): + code = re.split(r"-|_", code)[0] if code else "" + if tag_is_valid(code): + return standardize_tag(code) + return "" + + +class RetryableError(Exception): + """Recoverable error without having to modify the data state on the client + side, e.g. timeouts, errors from network partitioning, etc. + """ + + +class NonRetryableError(Exception): + """Recoverable error without having to modify the data state on the client + side, e.g. timeouts, errors from network partitioning, etc. + """ + + +def _add_param(params, name, value): + if value: + params[name] = value + return params + + +@retry( + retry=retry_if_exception_type(RetryableError), + wait=wait_exponential(multiplier=1, min=1, max=5), + stop=stop_after_attempt(5), + reraise=True, +) +def post_data( + url, + auth=None, + data=None, + files=None, + headers=None, + json=False, + timeout=2, + verify=True, +): + """ + Post data with HTTP + Retry: Wait 2^x * 1 second between each retry starting with 4 seconds, + then up to 10 seconds, then 10 seconds afterwards + Args: + url: URL address + files: files + headers: HTTP headers + json: True|False + verify: Verify the SSL. + Returns: + Return a requests.response object. + Except: + Raise a RetryableError to retry. + """ + try: + params = dict( + headers=headers, + timeout=timeout, + verify=verify, + ) + params = _add_param(params, "auth", auth) + params = _add_param(params, "files", files) + params = _add_param(params, "data", data) + response = requests.post(url, **params) + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as exc: + logger.error("Erro posting data (timeout=%s): %s, retry..., erro: %s" % (timeout, url, exc)) + raise RetryableError(exc) from exc + except ( + requests.exceptions.InvalidSchema, + requests.exceptions.MissingSchema, + requests.exceptions.InvalidURL, + ) as exc: + raise NonRetryableError(exc) from exc + try: + response.raise_for_status() + except requests.HTTPError as exc: + if response := is_http_error_json_response(json, response): + return response + if 400 <= exc.response.status_code < 500: + raise NonRetryableError(exc) from exc + elif 500 <= exc.response.status_code < 600: + logger.error( + "Erro fetching the content: %s, retry..., erro: %s" % (url, exc) + ) + raise RetryableError(exc) from exc + else: + raise + + return response.content if not json else response.json() + + +def is_http_error_json_response(json, response): + """ + Algumas API, por exemplo, opac_5, retornam a mensagem de erro em formato + JSON + """ + if not json: + return + + try: + return response.json() + except Exception: + return + + +@retry( + retry=retry_if_exception_type(RetryableError), + wait=wait_exponential(multiplier=1, min=1, max=5), + stop=stop_after_attempt(5), + reraise=True, +) +def fetch_data(url, params=None, headers=None, json=False, timeout=2, verify=True): + """ + Get the resource with HTTP + Retry: Wait 2^x * 1 second between each retry starting with 4 seconds, + then up to 10 seconds, then 10 seconds afterwards + Args: + url: URL address + headers: HTTP headers + json: True|False + verify: Verify the SSL. + Returns: + Return a requests.response object. + Except: + Raise a RetryableError to retry. + """ + + try: + logger.info("Fetching the URL: %s %s" % (url, params)) + response = requests.get( + url, params=params, headers=headers, timeout=timeout, verify=verify + ) + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as exc: + logger.error("Erro fetching the content: %s, retry..., erro: %s" % (url, exc)) + raise RetryableError(exc) from exc + except ( + requests.exceptions.InvalidSchema, + requests.exceptions.MissingSchema, + requests.exceptions.InvalidURL, + ) as exc: + raise NonRetryableError(exc) from exc + try: + response.raise_for_status() + except requests.HTTPError as exc: + if 400 <= exc.response.status_code < 500: + raise NonRetryableError(exc) from exc + elif 500 <= exc.response.status_code < 600: + logger.error( + "Erro fetching the content: %s, retry..., erro: %s" % (url, exc) + ) + raise RetryableError(exc) from exc + else: + raise + + return response.content if not json else response.json() + + +def _get_user(request, username=None, user_id=None): + try: + return User.objects.get(pk=request.user_id) + except AttributeError: + if user_id: + return User.objects.get(pk=user_id) + if username: + return User.objects.get(username=username) diff --git a/core/utils/sync_state.py b/core/utils/sync_state.py new file mode 100644 index 0000000..e093337 --- /dev/null +++ b/core/utils/sync_state.py @@ -0,0 +1,49 @@ +from django.utils import timezone +from django.utils.dateparse import parse_datetime + + +def _normalize_datetime(value): + if value is None: + return None + if hasattr(value, "utcoffset"): + dt = value + else: + dt = parse_datetime(str(value)) + if dt is None: + return None + if timezone.is_naive(dt): + dt = timezone.make_aware(dt, timezone.utc) + return dt + + +def track_max_from_item(current_max, item, field="updated"): + """ + Retorna o timestamp mais recente encontrado ao iterar resultados da API. + + Converte ``item[field]`` e ``current_max`` para ``datetime`` antes de + comparar, evitando erro ao misturar string ISO da API com ``DateTimeField``. + + Args: + current_max: ``datetime`` já processado, ou None. + item: Dicionário retornado pela API Core. + field: Nome do campo de data em ``item`` (padrão: ``created``). + + Returns: + O ``datetime`` mais recente entre ``current_max`` e ``item[field]``. + """ + value = _normalize_datetime(item.get(field)) + current_max = _normalize_datetime(current_max) + if value and (current_max is None or value > current_max): + return value + return current_max + + +def finalize_core_sync_state(sync_state, max_updated_at): + """ + Persiste o checkpoint após uma execução bem-sucedida de sync da API Core. + + Args: + sync_state: Instância de ``CoreSyncState`` do recurso sincronizado. + max_updated_at: Maior ``created`` (ou equivalente) visto na execução. + """ + sync_state.update_checkpoint(max_updated_at) diff --git a/core/wagtail_hooks.py b/core/wagtail_hooks.py new file mode 100644 index 0000000..8d2dd0a --- /dev/null +++ b/core/wagtail_hooks.py @@ -0,0 +1,90 @@ +import os + +from django.db.models.signals import pre_save +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from wagtail import hooks +from wagtail.admin.menu import Menu, MenuItem, SubmenuMenuItem +from wagtail.images import get_image_model + +from core.celery_wagtail import * # noqa: F401,F403 + + +def ensure_image_title(sender, instance, **kwargs): + if (instance.title or "").strip(): + return + if not instance.file: + return + basename = os.path.basename(instance.file.name) + instance.title = os.path.splitext(basename)[0] + + +pre_save.connect(ensure_image_title, sender=get_image_model()) + + +@hooks.register("construct_main_menu") +def group_wagtail_cms_menu_items(request, menu_items): + cms_item_names = {"explorer", "images", "documents", "xml_sps"} + menu_items[:] = [item for item in menu_items if item.name not in cms_item_names] + cms_menu = Menu( + items=[ + MenuItem( + _("Pages"), + reverse("wagtailadmin_explore_root"), + name="cms_pages", + icon_name="doc-empty-inverse", + order=100, + ), + MenuItem( + _("Images"), + reverse("wagtailimages:index"), + name="cms_images", + icon_name="image", + order=200, + ), + MenuItem( + _("Documents"), + reverse("wagtaildocs:index"), + name="cms_documents", + icon_name="doc-full-inverse", + order=300, + ), + ] + ) + menu_items.append( + SubmenuMenuItem( + _("Content Manager"), + cms_menu, + icon_name="folder-open-inverse", + name="wagtail_cms", + order=800, + ) + ) + settings_index = next( + (index for index, item in enumerate(menu_items) if item.name == "settings"), + None, + ) + report_index = next( + (index for index, item in enumerate(menu_items) if item.name == "reports"), + None, + ) + if settings_index is not None and report_index is not None: + reports_item = menu_items.pop(report_index) + if report_index < settings_index: + settings_index -= 1 + menu_items.insert(settings_index + 1, reports_item) + + +@hooks.register("construct_help_menu") +def replace_help_menu_items(request, help_menu_items): + help_menu_items[:] = [ + MenuItem( + _("Project Wiki"), + "https://github.com/scieloorg/scielo-tools/wiki", + name="project_wiki", + icon_name="link-external", + attrs={"target": "_blank", "rel": "noopener noreferrer"}, + order=100, + ) + ] + diff --git a/core_settings/__init__.py b/core_settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core_settings/migrations/0001_initial.py b/core_settings/migrations/0001_initial.py new file mode 100644 index 0000000..6cba432 --- /dev/null +++ b/core_settings/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# Generated by Django 6.0.5 on 2026-06-09 00:35 + +import django.db.models.deletion +import wagtail.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('wagtailcore', '0097_baselogentry_uuid_action_timestamp_indexes'), + ('wagtailimages', '0027_image_description'), + ] + + operations = [ + migrations.CreateModel( + name='CustomSettings', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, max_length=100, null=True)), + ('email', models.EmailField(blank=True, max_length=100, null=True)), + ('phone', models.CharField(blank=True, max_length=100, null=True)), + ('footer_text', wagtail.fields.RichTextField(blank=True, null=True)), + ('admin_logo', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image')), + ('favicon', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image')), + ('site', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, to='wagtailcore.site')), + ('site_logo', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image')), + ], + options={ + 'verbose_name': 'Site configuration', + 'verbose_name_plural': 'Site configuration', + }, + ), + ] diff --git a/core_settings/migrations/__init__.py b/core_settings/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core_settings/models.py b/core_settings/models.py new file mode 100644 index 0000000..8be2016 --- /dev/null +++ b/core_settings/models.py @@ -0,0 +1,69 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ +from wagtail.admin.panels import FieldPanel, ObjectList, TabbedInterface +from wagtail.contrib.settings.models import BaseSiteSetting, register_setting +from wagtail.fields import RichTextField + + +@register_setting +class CustomSettings(BaseSiteSetting): + """ + This a settings model. + + More about look: + https://docs.wagtail.org/en/stable/reference/contrib/settings.html + """ + + class Meta: + verbose_name = _("Site configuration") + verbose_name_plural = _("Site configuration") + + name = models.CharField(max_length=100, null=True, blank=True) + email = models.EmailField(max_length=100, null=True, blank=True) + phone = models.CharField(max_length=100, null=True, blank=True) + + footer_text = RichTextField(null=True, blank=True) + + favicon = models.ForeignKey( + "wagtailimages.Image", + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="+", + ) + + admin_logo = models.ForeignKey( + "wagtailimages.Image", + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="+", + ) + + site_logo = models.ForeignKey( + "wagtailimages.Image", + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="+", + ) + + site_panels = [ + FieldPanel("name"), + FieldPanel("email"), + FieldPanel("phone"), + FieldPanel("footer_text", classname="full"), + FieldPanel("favicon"), + FieldPanel("site_logo"), + ] + + admin_panels = [ + FieldPanel("admin_logo"), + ] + + edit_handler = TabbedInterface( + [ + ObjectList(site_panels, heading=_("Site settings")), + ObjectList(admin_panels, heading=_("Admin settings")), + ] + ) diff --git a/core_settings/static/core_settings/css/admin_logo.css b/core_settings/static/core_settings/css/admin_logo.css new file mode 100644 index 0000000..3b39249 --- /dev/null +++ b/core_settings/static/core_settings/css/admin_logo.css @@ -0,0 +1,12 @@ +.custom-admin-logo { + display: block; + max-height: 4rem; + max-width: 100%; + width: auto; + height: auto; +} + +.custom-admin-logo--login { + max-height: 5rem; + margin: 0 auto; +} diff --git a/core_settings/static/core_settings/img/favicon.ico b/core_settings/static/core_settings/img/favicon.ico new file mode 100644 index 0000000..409a122 Binary files /dev/null and b/core_settings/static/core_settings/img/favicon.ico differ diff --git a/core_settings/templates/wagtailadmin/base.html b/core_settings/templates/wagtailadmin/base.html new file mode 100644 index 0000000..4ad44ee --- /dev/null +++ b/core_settings/templates/wagtailadmin/base.html @@ -0,0 +1,5 @@ +{% extends "wagtailadmin/base.html" %} + +{% block branding_logo %} + {% include "wagtailadmin/includes/admin_logo.html" %} +{% endblock %} diff --git a/core_settings/templates/wagtailadmin/includes/admin_logo.html b/core_settings/templates/wagtailadmin/includes/admin_logo.html new file mode 100644 index 0000000..0669604 --- /dev/null +++ b/core_settings/templates/wagtailadmin/includes/admin_logo.html @@ -0,0 +1,9 @@ +{% if settings.core_settings.customsettings.admin_logo %} + +{% else %} + {% include "wagtailadmin/logo.html" %} +{% endif %} diff --git a/core_settings/templates/wagtailadmin/includes/admin_logo_login.html b/core_settings/templates/wagtailadmin/includes/admin_logo_login.html new file mode 100644 index 0000000..1dbf581 --- /dev/null +++ b/core_settings/templates/wagtailadmin/includes/admin_logo_login.html @@ -0,0 +1,9 @@ +{% if settings.core_settings.customsettings.admin_logo %} + +{% else %} + {% include "wagtailadmin/logo.html" with wordmark="True" %} +{% endif %} diff --git a/core_settings/templates/wagtailadmin/login.html b/core_settings/templates/wagtailadmin/login.html new file mode 100644 index 0000000..bcb41be --- /dev/null +++ b/core_settings/templates/wagtailadmin/login.html @@ -0,0 +1,7 @@ +{% extends "wagtailadmin/login.html" %} + +{% block branding_logo %} + +{% endblock %} diff --git a/core_settings/wagtail_hooks.py b/core_settings/wagtail_hooks.py new file mode 100644 index 0000000..4afa96c --- /dev/null +++ b/core_settings/wagtail_hooks.py @@ -0,0 +1,12 @@ +from django.templatetags.static import static +from django.utils.html import format_html +from wagtail import hooks + + +@hooks.register("insert_global_admin_css") +def admin_logo_css(): + return format_html( + '', + static("core_settings/css/admin_logo.css"), + static("core_settings/img/favicon.ico"), + ) diff --git a/docs/pr/2026-06-16-primeira-versao.md b/docs/pr/2026-06-16-primeira-versao.md new file mode 100644 index 0000000..3d34ece --- /dev/null +++ b/docs/pr/2026-06-16-primeira-versao.md @@ -0,0 +1,152 @@ +# Insumo de PR — primeira versão (MarkAPI / SciELO Tools) + +**Data:** 2026-06-16 +**Contexto:** entrega inicial da plataforma web para produção, validação e rastreio de artigos acadêmicos em **SPS XML**. + +--- + +## Mensagem de commit + +``` +Entrega primeira versão da plataforma MarkAPI (SciELO Tools) + +Implementa o fluxo editorial central: ingestão DOCX/XML/ZIP, marcação +assistida por IA, geração e validação SPS, artefatos HTML/PDF, rastreio +de eventos no pipeline e interface Wagtail com API REST para integração. +``` + +--- + +## O que esse PR faz? + +Esta é a **primeira versão funcional** da plataforma descrita no SciELO Research Communication Tools (RCT): um backbone editorial agnóstico a sistemas externos, com o **XML SPS como registro único** de cada objeto e de cada etapa do ciclo. + +**Objetivo atendido nesta entrega:** permitir que uma redação produza, valide e acompanhe artigos acadêmicos — da submissão (upload) à disseminação em múltiplos formatos — com marcação assistida por IA e revisão humana antes de finalizar. + +### Capacidades entregues + +| Área | O que a aplicação faz | +|------|------------------------| +| **Ingestão** | Upload de DOCX, XML ou ZIP via Wagtail; inspeção automática do tipo de entrada e sugestão de ações | +| **Marcação** | Identificação de citações e referências; extração de estrutura (front/body/back); enriquecimento de metadados via LLM | +| **XML SPS** | Geração de XML a partir da estrutura editável; validação com packtools; pacote `.zip` SPS com assets | +| **Derivados** | HTML e PDF a partir do XML; artefatos versionados (`is_current` / `is_stale`) | +| **Rastreio** | Cada ação do pipeline registrada como `ProcessingEvent` (status, agente, timestamp, detalhes) | +| **Referências** | App `references` com parsing, deduplicação e API REST; bases para normalização editorial | +| **Integração** | API REST (DRF/JWT) para sistemas externos; Wagtail embutido para quem não possui sistema editorial | +| **Infra** | Docker Compose (Django, Celery, Redis, PostgreSQL); LLM configurável (local ou remoto) | + +### Apps principais + +- **`manuscripts`** — pipeline editorial (upload → inspeção → processamento → estrutura → artefatos) +- **`sps`** — geração e validação SPS, HTML e PDF (packtools + LibreOffice) +- **`ai`** — serviço unificado de LLM (Gemini, Ollama, HuggingFace) +- **`references`**, **`journals`**, **`docx_parser`**, **`labeling`**, **`core`**, **`users`** + +### Limitações conhecidas desta versão + +- Foco inicial em **artigos/manuscritos** (DOCX/XML); os seis tipos de objeto do RCT (preprint, dado de pesquisa, livro, capítulo) ainda não estão cobertos de forma completa. +- Avaliação informada (checklists CONSORT, PRISMA, FAIR) e integrações SciELO (Upload/OPAC 5) estão no roadmap, não nesta entrega. +- Pipeline executa ações **sequencialmente**; retry granular por ação via Celery encadeado é melhoria futura. + +--- + +## Onde a revisão poderia começar? + +1. **`README.md`** — visão geral, pipeline em 9 etapas e mapa de apps. +2. **`manuscripts/processing.py`** — orquestrador do pipeline (`process_input`). +3. **`manuscripts/utils/processing_actions.py`** — handlers por ação (citação, XML, validação, HTML, PDF, pacote SPS). +4. **`manuscripts/controller.py`** — lógica de artigos, eventos e estrutura. +5. **`manuscripts/wagtail_hooks.py`** + **`manuscripts/views/`** — interface editorial no Wagtail. +6. **`sps/`** — camada de conformidade SPS (geração, validação, renderização). + +--- + +## Como este poderia ser testado manualmente? + +### 1. Subir o ambiente + +```bash +make build +make up +make django_migrate +make django_createsuperuser # se ainda não existir usuário admin +``` + +Serviços locais: Wagtail em http://127.0.0.1:8009 (ver `README.md` para demais portas). + +### 2. Fluxo editorial completo (Wagtail) + +1. Aceder ao admin Wagtail com o superusuário. +2. Criar ou abrir um **Processing** e fazer upload de um DOCX ou XML de exemplo. +3. Confirmar a **inspeção**: tipo detectado e ações sugeridas. +4. Iniciar o **processamento** e acompanhar o estado (`ProcessStatus`) e os eventos registados. +5. Rever a **estrutura** do artigo (front/body/back) e metadados extraídos. +6. Verificar **artefatos** gerados: XML SPS, relatório de validação, pacote ZIP, HTML e PDF. +7. Confirmar que cada etapa aparece em **ProcessingEvent** com status e mensagem coerentes. + +### 3. API REST + +1. Obter token JWT para um utilizador de teste. +2. Chamar endpoints documentados em `/api/v1/` (ex.: operações sobre artigos e referências). +3. Confirmar autenticação, paginação e respostas esperadas. + +### 4. Referências + +1. No fluxo de estrutura, validar ligação entre citações no corpo e blocos de referência. +2. Testar endpoints do app `references` (listagem, parsing, deduplicação) via API ou admin. + +### 5. Validação automatizada (complementar) + +```bash +make test # suíte pytest do app manuscripts +make test-fast # fail-fast, útil durante revisão de código +``` + +Estes comandos validam regressões no pipeline e nas views; **não substituem** o teste manual do fluxo editorial descrito acima. + +--- + +## Algum cenário de contexto que queira dar? + +A plataforma nasce para ser **integração aditiva**: periódicos e editoras mantêm OJS, ScholarOne ou fluxos próprios; o MarkAPI actua como backbone via API REST, registando tudo no XML SPS. + +**Princípios desta versão:** + +- A IA **auxilia** a marcação (referências bibliográficas em destaque); o humano **revisa e corrige** antes de finalizar. +- Conformidade SPS garantida por **packtools** (versão fixada no projeto). +- LLM preferencialmente **on-premise** (`Dockerfile.llama`); APIs externas são responsabilidade da instituição. +- Correções no XML propagam-se aos derivados (HTML, PDF, pacote SPS). + +Esta entrega materializa o núcleo do RCT — conversão, marcação, validação, múltiplos formatos e rastreio — numa stack Django/Wagtail implantável com Docker Compose. + +--- + +### Screenshots + +Recomendado anexar no PR do GitHub, se possível: + +- Tela de upload/inspeção de um Processing no Wagtail +- Lista de eventos do pipeline e artefatos gerados (XML, HTML, PDF) +- Exemplo de estrutura editável (front/body/back) + +--- + +## Quais são tickets relevantes? + +N/A + +--- + +### Referências + +- SciELO Research Communication Tools (RCT) v4.0 — `scielo_rct_v3_updated.docx` +- Repositório: [scieloorg/SciELO-Tools](https://github.com/scieloorg/scielo-tools) +- Wiki — [modelo LLM e configuração](https://github.com/scieloorg/scielo-tools/wiki) +- `README.md` — pipeline, apps, desenvolvimento e testes + +--- + +## Correção de segurança (pré-push) + +Removida `DJANGO_SECRET_KEY` real hardcoded em `config/settings/local.py`; o default local passou a ser `django-insecure-local-dev-only`. Produção continua a exigir `DJANGO_SECRET_KEY` via `.envs` (não versionado). diff --git a/docx_parser/__init__.py b/docx_parser/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docx_parser/layouts/two_cols.docx b/docx_parser/layouts/two_cols.docx new file mode 100644 index 0000000..25b4aea Binary files /dev/null and b/docx_parser/layouts/two_cols.docx differ diff --git a/docx_parser/omml2mml.xsl b/docx_parser/omml2mml.xsl new file mode 100644 index 0000000..dcd23ee --- /dev/null +++ b/docx_parser/omml2mml.xsl @@ -0,0 +1,2068 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ˘ + + ¸ + + ` + + - + + + + . + + ˙ + + ˝ + + ´ + + ~ + + ˜ + + ¨ + + ˇ + + ^ + + ¯ + + _ + + + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + left + + + right + + + + + + + + + + + + + + + + 0 + + + + + + + + + + 0 + + + + + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0in + + + 0in + + + 0in + + + + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + false + + + + + + + + + + + + + + + + + + + off + + + + + + + + + + + off + + + + + + + + + + + + + + off + + + + + + + + + 1 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + / + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + 0pt + + + + right + + + left + + + + + right + + + left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 0 + + + + + + + + + + ¯ + + + + + + + + + + _ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + + + + + normal + + + + + + + + monospace + sans-serif-italic + bold-sans-serif + sans-serif-bold-italic + sans-serif + bold-fraktur + fraktur + double-struck + bold-script + script + bold + italic + normal + bold-italic + + + + + + bold + normal + + + + + normal + italic + + + + + + + + + + + + + + + + + + + + + + + + italic + + + bold + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 1 + + + + + + + + + + + + + + + + + + + + + + 1 + 0 + + + + + 1 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 0 + + + + + 1 + 0 + + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + box + + + + + + + + + + + + + + + + + left right top bottom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 0 + -1 + + + + + + + + + + + + + 0 + 1 + + + + + + + + + + + + + 1 + 0 + + + \ No newline at end of file diff --git a/docx_parser/parser.py b/docx_parser/parser.py new file mode 100644 index 0000000..41881ac --- /dev/null +++ b/docx_parser/parser.py @@ -0,0 +1,390 @@ +import html +import os +import re +import zipfile + +import docx +from django.core.files.base import ContentFile +from docx.oxml.ns import qn +from docx.oxml.table import CT_Tbl +from docx.oxml.text.paragraph import CT_P +from lxml import etree, objectify +from wagtail.images import get_image_model + +ImageModel = get_image_model() + +WML_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" +DRAWINGML_NS = "http://schemas.openxmlformats.org/drawingml/2006/main" +REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" +MATH_NS = "http://schemas.openxmlformats.org/officeDocument/2006/math" +MATHML_NS = "http://www.w3.org/1998/Math/MathML" +PACKAGE_NS = "http://schemas.openxmlformats.org/package/2006/relationships" + + +class DocxParser: + + @staticmethod + def open_docx(filename): + return docx.Document(filename) + + def replace_mfenced_pipe(self, mathml_root): + nsmap = {"mml": MATHML_NS} + for mfenced in mathml_root.xpath(".//mml:mfenced", namespaces=nsmap): + if mfenced.get("open") or mfenced.get("close"): + continue + if mfenced.get("separators", "") != "|": + continue + mrow = etree.Element(f"{{{MATHML_NS}}}mrow") + mo_open = etree.SubElement(mrow, f"{{{MATHML_NS}}}mo") + mo_open.text = "(" + mo_close = etree.SubElement(mrow, f"{{{MATHML_NS}}}mo") + mo_close.text = ")" + for child in list(mfenced): + mrow.append(child) + parent = mfenced.getparent() + if parent is not None: + parent.replace(mfenced, mrow) + return mathml_root + + def _read_numbering(self, docx_path): + mapping = {} + with zipfile.ZipFile(docx_path, "r") as archive: + if "word/numbering.xml" not in archive.namelist(): + return None + tree = etree.fromstring(archive.read("word/numbering.xml")) + nsmap = tree.nsmap + w_ns = f"{{{WML_NS}}}" + for abstract in tree.findall(f".//{w_ns}abstractNum" if WML_NS in str(nsmap) else ".//w:abstractNum", namespaces=nsmap): + aid = abstract.get(f"{w_ns}abstractNumId" if WML_NS in str(nsmap) else next(iter(nsmap)) + "}abstractNumId", abstract.get("abstractNumId", "")) + if aid not in mapping: + mapping[aid] = {} + for lvl in abstract.findall(f".//{w_ns}lvl" if WML_NS in str(nsmap) else ".//w:lvl", namespaces=nsmap): + ilvl = lvl.get(f"{w_ns}ilvl" if WML_NS in str(nsmap) else "ilvl", lvl.get("ilvl", "0")) + fmt_el = lvl.find(f".//{w_ns}numFmt" if WML_NS in str(nsmap) else ".//w:numFmt", namespaces=nsmap) + fmt = fmt_el.get(f"{w_ns}val" if WML_NS in str(nsmap) else "val", "bullet") if fmt_el is not None else "bullet" + mapping[aid][ilvl] = fmt + for num in tree.findall(f".//{w_ns}num" if WML_NS in str(nsmap) else ".//w:num", namespaces=nsmap): + num_id = num.get(f"{w_ns}numId" if WML_NS in str(nsmap) else "numId", num.get("numId", "")) + ref_el = num.find(f".//{w_ns}abstractNumId" if WML_NS in str(nsmap) else ".//w:abstractNumId", namespaces=nsmap) + if ref_el is not None: + ref_aid = ref_el.get(f"{w_ns}val" if WML_NS in str(nsmap) else "val", ref_el.get("val", "")) + if ref_aid in mapping: + mapping[ref_aid]["numId"] = num_id + return mapping + + def _read_hyperlinks(self, docx_path): + links = {} + with zipfile.ZipFile(docx_path, "r") as archive: + rels_path = "word/_rels/document.xml.rels" + if rels_path not in archive.namelist(): + return links + rels_root = etree.fromstring(archive.read(rels_path)) + for rel in rels_root: + r_id = rel.get("Id", "") + target = rel.get("Target", "") + if "hyperlink" in rel.get("Type", ""): + links[r_id] = target + return links + + def _extract_hyperlinks(self, element, rels_map): + links = [] + raw = etree.fromstring(etree.tostring(element)) + for hyperlink in raw.xpath(".//w:hyperlink|.//a:hlinkClick", namespaces={ + "w": WML_NS, "a": DRAWINGML_NS, + }): + r_id = hyperlink.get(f"{{{REL_NS}}}id", "") + if r_id and r_id in rels_map: + links.append(rels_map[r_id]) + return " ".join(links) if links else None + + + def extract_content(self, doc, doc_path, merge_front=True): + list_types = self._read_numbering(doc_path) + self._read_hyperlinks(doc_path) + + BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + xslt_path = os.path.join(BASE_DIR, "omml2mml.xsl") + transform = etree.XSLT(etree.parse(xslt_path)) + + def _match_paragraph(text): + if re.search(r"(?im)^\s*(?:)?\s*(palabra(?:s)?\s*clave|palavras?\s*-?\s*chave|keywords?)\s*(?:)?\s*(?::|\s*:\s*)\s*(.+)$", text): + return "" + if re.search(r"(?i)^resumen|^resumo|^abstract", text): + return "" + if re.search(r"(?i)aceptado|accepted|aceited|aprovado", text): + return "" + if re.search(r"(?i)recibido|received|recebido", text): + return "" + return False + + def _matches_section(a, b): + try: + return a.get("size") == b.get("size") and a.get("bold") == b.get("bold") and a.get("isupper") == b.get("isupper") + except Exception: + return False + + def _identify_section(sections, size, bold, text): + if size == 0: + return sections + isupper = text.isupper() + s = {"size": size, "bold": bold, "isupper": isupper, "count": 0} + if not sections: + sections.append(s) + return sections + for existing in sections: + if _matches_section(s, existing): + existing["count"] += 1 + return sections + sections.append(s) + return sections + + def _clean_labels(text): + text = re.sub(r"\[\s*/?\s*[\w-]+(?:\s+[^\]]+)?\s*\]", "", text) + text = re.sub(r"\s+", " ", text) + text = re.sub(r"\s+([;:,.])", r"\1", text) + return text.strip() + + + def _section_priority(sections): + return (-sections["size"], not sections["bold"], not sections["isupper"]) + + + def _extract_table(element): + def _cell_text(cell): + p_texts = [] + for p in cell.xpath(".//w:p"): + parts = [] + for child in p.xpath(".//w:t | .//w:br"): + if child.tag.endswith("t"): + parts.append(html.escape(child.text or "", quote=False)) + elif child.tag.endswith("br"): + parts.append("
") + pt = "".join(parts) + if pt.strip() or "
" in pt: + p_texts.append(pt) + if not p_texts: + return html.escape("".join(t.text or "" for t in cell.xpath(".//w:t")), quote=False) + return "
".join(p_texts) + + table_html = '\n' + rowspan_map = {} + rows = element.xpath(".//w:tr") + for i, row in enumerate(rows): + table_html += " \n" + j = 0 + for cell in row.xpath(".//w:tc"): + while (i, j) in rowspan_map and rowspan_map[(i, j)] > 0: + rowspan_map[(i, j)] -= 1 + j += 1 + + rs = 1 + cs = 1 + vmerge_skip = False + + vm = cell.xpath(".//w:vMerge") + if vm: + val = vm[0].get(qn("w:val")) + if val == "restart": + rs = 1 + k = i + 1 + while k < len(rows): + try: + nc = rows[k].xpath(".//w:tc")[j] + nvm = nc.xpath(".//w:vMerge") + except (IndexError, AttributeError): + break + if nvm and nvm[0].get(qn("w:val")) is None: + rs += 1 + else: + break + k += 1 + for d in range(rs): + rowspan_map[(i + d, j)] = rs - d - 1 + else: + vmerge_skip = True + + gs = cell.xpath(".//w:gridSpan") + if gs: + cs = int(gs[0].get(qn("w:val"))) + + if not vmerge_skip: + ct = _clean_labels(_cell_text(cell)) + tag = "th" if i == 0 else "td" + attrs = "" + if rs > 1: + attrs += f' rowspan="{rs}"' + if cs > 1: + attrs += f' colspan="{cs}"' + table_html += f" <{tag}{attrs}>{ct}\n" + j += 1 + (cs - 1) + table_html += " \n" + table_html += "
" + return table_html + + + content = [] + sections = [] + images = [] + found_fb = False + review_fb = True + start_markers = ["introducción", "introduction", "introdução"] + + current_list = [] + current_num_id = None + + for element in doc.element.body: + is_list_item = False + if isinstance(element, CT_P): + obj = {} + paragraph = element + text_parts = [] + + _ns = {"w": WML_NS} + is_list_item = paragraph.find(".//w:numPr", namespaces=_ns) is not None + + obj_image = False + for drawing in element.findall(".//w:drawing", namespaces={"w": WML_NS, "a": DRAWINGML_NS}): + blip = drawing.find(".//a:blip", namespaces={"w": WML_NS, "a": DRAWINGML_NS}) + if blip is not None: + obj_image = True + r_id = blip.get(f"{{{REL_NS}}}embed") + image_part = doc.part.related_parts[r_id] + image_data = image_part.blob + image_name = image_part.partname.split("/")[-1] + if image_name not in images: + images.append(image_name) + wagtail_image = ImageModel.objects.create( + title=image_name, + file=ContentFile(image_data, name=image_name), + ) + obj["type"] = "image" + obj["image"] = wagtail_image.id + + ns_m = {"m": MATH_NS, "w": WML_NS} + for formula in element.findall(".//m:oMathPara", namespaces=ns_m): + obj_image = True + mathml_result = transform(formula) + mathml_root = etree.fromstring(str(mathml_result)) + mathml_root = self.replace_mfenced_pipe(mathml_root) + obj["type"] = "formula" + obj["formula"] = etree.tostring(mathml_root, pretty_print=True, encoding="unicode") + + if not obj_image: + if is_list_item: + numPr_el = paragraph.find(".//w:numPr", namespaces=_ns) + num_id_val = numPr_el.find(".//w:numId", namespaces=_ns).get(qn("w:val")) if numPr_el is not None else None + list_type = "bullet" + if list_types and num_id_val: + for _k, info in list_types.items(): + if info.get("numId") == num_id_val: + if info.get("0") == "decimal": + list_type = "order" + break + if num_id_val != current_num_id: + current_num_id = num_id_val + if current_list: + current_list.append("[/list]") + content.append({"type": "list", "list": "\n".join(current_list)}) + current_list = [] + current_list.append(f'[list list-type="{list_type}"]') + else: + if current_list: + current_list.append("[/list]") + content.append({"type": "list", "list": "\n".join(current_list)}) + current_list = [] + + for child in paragraph: + w_ns = f"{{{WML_NS}}}" + if child.tag == f"{w_ns}hyperlink": + for r in child.findall("w:r", namespaces=_ns): + t_el = r.find("w:t", namespaces=_ns) + if t_el is not None and t_el.text: + text_parts.append(t_el.text) + elif child.tag == f"{w_ns}r": + sz = child.find(".//w:sz", namespaces=_ns) + if sz is None: + sz = paragraph.find(".//w:rPr/w:sz", namespaces=_ns) + obj["font_size"] = int(objectify.fromstring(etree.tostring(sz, encoding="unicode")).get(qn("w:val"))) / 2 if sz is not None else 0 + + b_tag = child.find(".//w:b", namespaces=_ns) + if b_tag is None: + b_tag = paragraph.find(".//w:rPr/w:b", namespaces=_ns) + obj["bold"] = b_tag is not None and b_tag.get(qn("w:val"), "1") in (None, "1", "true", "True") + + i_tag = child.find(".//w:i", namespaces=_ns) + if i_tag is None: + i_tag = paragraph.find(".//w:rPr/w:i", namespaces=_ns) + obj["italic"] = i_tag is not None and i_tag.get(qn("w:val"), "1") in (None, "1", "true", "True") + + cleaned = _clean_labels(child.text or "") + + sections = _identify_section(sections, obj.get("font_size", 0), obj.get("bold", False), cleaned) + + if obj.get("italic"): + text_parts.append(f"{cleaned}") + else: + text_parts.append(cleaned) + + if _match_paragraph(cleaned): + obj["paraph"] = _match_paragraph(cleaned) + obj["type"] = obj["paraph"] + + if review_fb: + found_fb = any(w in cleaned.lower() for w in start_markers) + + if found_fb: + found_fb = False + review_fb = False + sections = [sections[-1]] if sections else [] + if merge_front: + fb_text = "" + tmp = [] + abstract_mode = False + for c in content: + if abstract_mode: + if not c.get("text") or c.get("spacing"): + abstract_mode = False + else: + tmp.append(c) + continue + if c.get("paraph"): + tmp.append(c) + abstract_mode = c["paraph"] == "" + else: + fb_text += "\n" + (c.get("text") or c.get("table") or "") + tmp.append({"type": "first_block", "text": fb_text}) + content = tmp + start_markers = [] + + if child.tag == f"{{{MATH_NS}}}oMath": + if "text" not in obj or not isinstance(obj["text"], list): + obj["type"] = "compound" + obj["text"] = [] + if text_parts: + obj["text"].append({"type": "text", "value": " ".join(text_parts)}) + text_parts = [] + mathml_result = transform(child) + mathml_root = etree.fromstring(str(mathml_result)) + self.replace_mfenced_pipe(mathml_root) + obj["text"].append({"type": "formula", "value": etree.tostring(mathml_root, pretty_print=True, encoding="unicode")}) + + if "text" not in obj: + obj["text"] = _clean_labels(" ".join(text_parts)) + if _match_paragraph(obj["text"]): + obj["paraph"] = _match_paragraph(obj["text"]) + obj["type"] = obj["paraph"] + if is_list_item: + obj.pop("font_size", None) + current_list.append(f'[list-item]{obj["text"]}[/list-item]') + if isinstance(obj.get("text"), list) and text_parts: + obj["text"].append({"type": "text", "value": " ".join(text_parts)}) + text_parts = [] + + elif isinstance(element, CT_Tbl): + obj = {"type": "table", "table": _extract_table(element)} + + if not is_list_item: + content.append(obj) + + sections.sort(key=_section_priority) + return sections, content diff --git a/journals/__init__.py b/journals/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/journals/__init__.py @@ -0,0 +1 @@ + diff --git a/journals/apps.py b/journals/apps.py new file mode 100644 index 0000000..7ee9750 --- /dev/null +++ b/journals/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class JournalsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "journals" + verbose_name = _("Journals") diff --git a/journals/migrations/0001_initial.py b/journals/migrations/0001_initial.py new file mode 100644 index 0000000..d8fb960 --- /dev/null +++ b/journals/migrations/0001_initial.py @@ -0,0 +1,52 @@ +# Generated by Django 6.0.5 on 2026-06-09 00:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Issue', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Last update date')), + ('number', models.CharField(blank=True, max_length=20, verbose_name='Number')), + ('volume', models.CharField(blank=True, max_length=20, verbose_name='Volume')), + ('season', models.CharField(blank=True, max_length=20, verbose_name='Season')), + ('year', models.CharField(blank=True, max_length=4, verbose_name='Year')), + ('month', models.CharField(blank=True, max_length=20, verbose_name='Month')), + ('supplement', models.CharField(blank=True, max_length=20, verbose_name='Supplement')), + ], + options={ + 'verbose_name': 'Issue', + 'verbose_name_plural': 'Issues', + 'ordering': ['-year', 'journal', 'volume', 'number'], + }, + ), + migrations.CreateModel( + name='Journal', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.TextField(unique=True, verbose_name='Title')), + ('short_title', models.TextField(blank=True, verbose_name='Short title')), + ('title_nlm', models.TextField(blank=True, verbose_name='NLM title')), + ('acronym', models.CharField(blank=True, max_length=50, verbose_name='Acronym')), + ('issn', models.CharField(blank=True, max_length=32, verbose_name='SciELO ISSN')), + ('pissn', models.CharField(blank=True, max_length=32, verbose_name='Print ISSN')), + ('eissn', models.CharField(blank=True, max_length=32, verbose_name='Electronic ISSN')), + ('publisher_name', models.TextField(blank=True, verbose_name='Publisher name')), + ], + options={ + 'verbose_name': 'Journal', + 'verbose_name_plural': 'Journals', + 'ordering': ['title'], + }, + ), + ] diff --git a/journals/migrations/0002_initial.py b/journals/migrations/0002_initial.py new file mode 100644 index 0000000..1369672 --- /dev/null +++ b/journals/migrations/0002_initial.py @@ -0,0 +1,33 @@ +# Generated by Django 6.0.5 on 2026-06-09 00:35 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('journals', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='issue', + name='creator', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_creator', to=settings.AUTH_USER_MODEL, verbose_name='Creator'), + ), + migrations.AddField( + model_name='issue', + name='updated_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_last_mod_user', to=settings.AUTH_USER_MODEL, verbose_name='Updater'), + ), + migrations.AddField( + model_name='issue', + name='journal', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issues', to='journals.journal', verbose_name='Journal'), + ), + ] diff --git a/journals/migrations/__init__.py b/journals/migrations/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/journals/migrations/__init__.py @@ -0,0 +1 @@ + diff --git a/journals/models.py b/journals/models.py new file mode 100644 index 0000000..7dc0cfa --- /dev/null +++ b/journals/models.py @@ -0,0 +1,81 @@ +from django.db import models +from django.db.models import Q +from django.utils.translation import gettext_lazy as _ +from modelcluster.models import ClusterableModel +from wagtail.admin.panels import FieldPanel +from wagtailautocomplete.edit_handlers import AutocompletePanel + +from core.models import CommonControlField + + +class Journal(models.Model): + title = models.TextField(_("Title"), unique=True) + short_title = models.TextField(_("Short title"), blank=True) + title_nlm = models.TextField(_("NLM title"), blank=True) + acronym = models.CharField(_("Acronym"), max_length=50, blank=True) + issn = models.CharField(_("SciELO ISSN"), max_length=32, blank=True) + pissn = models.CharField(_("Print ISSN"), max_length=32, blank=True) + eissn = models.CharField(_("Electronic ISSN"), max_length=32, blank=True) + publisher_name = models.TextField(_("Publisher name"), blank=True) + autocomplete_search_field = "title" + + def autocomplete_label(self): + return str(self) + + def __str__(self): + return self.title + + class Meta: + verbose_name = _("Journal") + verbose_name_plural = _("Journals") + ordering = ["title"] + + +class Issue(CommonControlField, ClusterableModel): + journal = models.ForeignKey( + Journal, on_delete=models.CASCADE, related_name="issues", verbose_name=_("Journal") + ) + number = models.CharField(_("Number"), max_length=20, blank=True) + volume = models.CharField(_("Volume"), max_length=20, blank=True) + season = models.CharField(_("Season"), max_length=20, blank=True) + year = models.CharField(_("Year"), max_length=4, blank=True) + month = models.CharField(_("Month"), max_length=20, blank=True) + supplement = models.CharField(_("Supplement"), max_length=20, blank=True) + panels = [ + AutocompletePanel("journal"), + FieldPanel("number"), + FieldPanel("volume"), + FieldPanel("season"), + FieldPanel("year"), + FieldPanel("month"), + FieldPanel("supplement"), + ] + autocomplete_search_field = "number" + + @classmethod + def autocomplete_custom_queryset_filter(cls, search_query): + return cls.objects.filter( + Q(number__icontains=search_query) + | Q(volume__icontains=search_query) + | Q(year__icontains=search_query) + | Q(journal__title__icontains=search_query) + ) + + def autocomplete_label(self): + return str(self) + + @property + def identifier(self): + return "".join( + f"{label}{value}" + for label, value in zip(("v", "n", "s"), (self.volume, self.number, self.supplement)) + if value + ) + + def __str__(self): + return f"{self.identifier} - {self.journal}" + + class Meta: + verbose_name = _("Issue") + verbose_name_plural = _("Issues") + ordering = ["-year", "journal", "volume", "number"] diff --git a/journals/sync_api.py b/journals/sync_api.py new file mode 100644 index 0000000..3d2389a --- /dev/null +++ b/journals/sync_api.py @@ -0,0 +1,244 @@ +import logging +from urllib.parse import urlencode + +from django.conf import settings +from django.db.models import Q + +from core.models import CoreSyncState +from core.utils.requester import fetch_data as fetch +from core.utils.sync_state import finalize_core_sync_state, track_max_from_item + +from .models import Issue, Journal + +logger = logging.getLogger(__name__) + + +def _iter_api_pages(url, resource_name): + while url: + logger.info(f"Syncing {resource_name} page: {url}") + + data = fetch( + url, headers={"Accept": "application/json"}, json=True, timeout=(10, 60) + ) + yield data.get("results", []) + url = data.get("next") + + +def _build_journal_from_api_item(item): + title = item.get("title") or "" + short_title = item.get("short_title") or "" + acronym = item.get("acronym") or "" + + official = item.get("official") or {} + pissn = official.get("issn_print") or "" + eissn = official.get("issn_electronic") or "" + + pubname = item.get("publisher", []) + title_in_database = item.get("title_in_database", []) + title_nlm = "" + + if title_in_database: + for t in title_in_database: + if t.get("name") == "MEDLINE": + title_nlm = t.get("title") or "" + + if pubname: + pubname = pubname[0].get("name") or "" + else: + pubname = "" + + scielo_journals = item.get("scielo_journal", []) + issn_scielo = "" + if scielo_journals: + issn_scielo = scielo_journals[0].get("issn_scielo") or "" + + return Journal( + title=title, + short_title=short_title, + acronym=acronym, + pissn=pissn, + eissn=eissn, + publisher_name=pubname, + title_nlm=title_nlm, + issn=issn_scielo, + ) + + +def build_api_url_core(domain, endpoint, params): + url = f"{domain}{endpoint}" + query = urlencode(params) + return f"{url}?{query}" + + +def sync_journals_from_api( + collection_acron=None, + issn_scielo=None, + from_date_updated=None, +): + sync_state = CoreSyncState.get_for_resource(resource="journal") + if from_date_updated is None: + from_date_updated = sync_state.get_from_date_updated( + settings.CORE_ISSUE_FROM_DATE_CREATED + ) + + params = {"from_date_updated": from_date_updated} + if collection_acron: + params["collection"] = collection_acron + if issn_scielo: + params["issn"] = issn_scielo + + url = build_api_url_core( + domain=settings.CORE_API_DOMAIN, + endpoint=settings.CORE_JOURNAL_API_ENDPOINT, + params=params, + ) + synced_count = 0 + skipped_count = 0 + max_created = sync_state.last_updated_at + + for items in _iter_api_pages(url, "journals"): + for item in items: + journal = _build_journal_from_api_item(item) + obj, _ = Journal.objects.update_or_create( + title=journal.title, + defaults={ + "short_title": journal.short_title, + "title_nlm": journal.title_nlm, + "acronym": journal.acronym, + "issn": journal.issn, + "pissn": journal.pissn, + "eissn": journal.eissn, + "publisher_name": journal.publisher_name, + }, + ) + logger.info(f"Journal {obj} completed") + synced_count += 1 + max_created = track_max_from_item(max_created, item) + + finalize_core_sync_state(sync_state, max_created) + logger.info( + f"Journal sync finished. Synced={synced_count} skipped={skipped_count}" + ) + + +def _get_journal_from_issue_data(issue_data): + journal_data = issue_data.get("journal") or {} + issn_values = [ + journal_data.get("issn_print"), + journal_data.get("issn_electronic"), + journal_data.get("scielo_journal"), + ] + issn_values = [v for v in issn_values if v] + + if not issn_values: + return None + + return ( + Journal.objects.filter( + Q(pissn__in=issn_values) + | Q(eissn__in=issn_values) + | Q(issn__in=issn_values) + ) + .order_by("id") + .first() + ) + + +def build_issue_from_data(item): + issue_data = { + "number": item.get("number") or "", + "volume": item.get("volume") or "", + "season": item.get("season") or "", + "year": item.get("year") or "", + "month": item.get("month") or "", + "supplement": item.get("supplement") or "", + } + return issue_data + + +def get_or_create_issue_from_api_data(issue_data): + lookup = { + "journal": issue_data["journal"], + "number": issue_data["number"], + "volume": issue_data["volume"], + "season": issue_data["season"], + "year": issue_data["year"], + "month": issue_data["month"], + "supplement": issue_data["supplement"], + } + queryset = Issue.objects.filter(**lookup).order_by("id") + issue = queryset.first() + if issue: + duplicates_count = queryset.count() + if duplicates_count > 1: + logger.warning( + "Issue sync found %s duplicated issues for journal_id=%s " + "volume=%r number=%r supplement=%r year=%r month=%r season=%r. " + "Using issue_id=%s.", + duplicates_count, + issue.journal_id, + issue.volume, + issue.number, + issue.supplement, + issue.year, + issue.month, + issue.season, + issue.id, + ) + return issue, False + return Issue.objects.create(**issue_data), True + + +def _get_registered_issn_scielo_values(issn_scielo=None): + queryset = Journal.objects.exclude(issn="") + if issn_scielo: + queryset = queryset.filter(issn=issn_scielo) + return queryset.values_list("issn", flat=True).distinct() + + +def sync_issues_from_api(issn_scielo=None, from_date_updated=None): + sync_state = CoreSyncState.get_for_resource(resource="issue") + if from_date_updated is None: + from_date_updated = sync_state.get_from_date_updated( + settings.CORE_ISSUE_FROM_DATE_CREATED + ) + + registered_issns = _get_registered_issn_scielo_values(issn_scielo=issn_scielo) + if not registered_issns: + logger.warning( + "Issue sync skipped: no registered journals found" + + (f" for issn_scielo={issn_scielo}" if issn_scielo else "") + ) + return + + synced_count = 0 + skipped_count = 0 + max_created = sync_state.last_updated_at + + for journal_issn in registered_issns: + url = build_api_url_core( + domain=settings.CORE_API_DOMAIN, + endpoint=settings.CORE_ISSUE_API_ENDPOINT, + params={ + "from_date_updated": from_date_updated, + "issn": journal_issn, + }, + ) + + for items in _iter_api_pages(url, f"issues ({journal_issn})"): + for item in items: + journal = _get_journal_from_issue_data(item) + if not journal: + skipped_count += 1 + continue + issue_data = build_issue_from_data(item) + issue_data.update({"journal": journal}) + get_or_create_issue_from_api_data(issue_data) + synced_count += 1 + max_created = track_max_from_item(max_created, item) + + finalize_core_sync_state(sync_state, max_created) + logger.info( + f"Issue sync finished. from_date_updated={from_date_updated} " + f"synced={synced_count} skipped={skipped_count}" + ) diff --git a/journals/tasks.py b/journals/tasks.py new file mode 100644 index 0000000..0571c7a --- /dev/null +++ b/journals/tasks.py @@ -0,0 +1,15 @@ +from config import celery_app + +from .sync_api import sync_issues_from_api, sync_journals_from_api + + +@celery_app.task() +def task_sync_journals_from_api(**kwargs): + kwargs.pop("user_id", None) + sync_journals_from_api(**kwargs) + + +@celery_app.task() +def task_sync_issues_from_api(**kwargs): + kwargs.pop("user_id", None) + sync_issues_from_api(**kwargs) diff --git a/journals/templates/wagtailadmin/icons/issue.svg b/journals/templates/wagtailadmin/icons/issue.svg new file mode 100644 index 0000000..34da1fb --- /dev/null +++ b/journals/templates/wagtailadmin/icons/issue.svg @@ -0,0 +1,3 @@ + + + diff --git a/journals/templates/wagtailadmin/icons/journal.svg b/journals/templates/wagtailadmin/icons/journal.svg new file mode 100644 index 0000000..042177f --- /dev/null +++ b/journals/templates/wagtailadmin/icons/journal.svg @@ -0,0 +1,3 @@ + + + diff --git a/journals/wagtail_hooks.py b/journals/wagtail_hooks.py new file mode 100644 index 0000000..1d4ac89 --- /dev/null +++ b/journals/wagtail_hooks.py @@ -0,0 +1,59 @@ +from django.utils.translation import gettext_lazy as _ +from wagtail import hooks +from wagtail.snippets.models import register_snippet +from wagtail.snippets.views.snippets import ( + CreateView, + SnippetViewSet, + SnippetViewSetGroup, +) + +from config.menu import get_menu_order + +from .models import Issue, Journal + + +class OwnedCreateView(CreateView): + def save_instance(self): + self.form.instance.creator = self.request.user + return super().save_instance() + + +class JournalViewSet(SnippetViewSet): + model = Journal + menu_label = _("Journals") + menu_icon = "journal" + menu_order = 20 + add_to_admin_menu = False + list_display = ("title", "acronym", "issn") + search_fields = ("title", "acronym", "issn", "pissn", "eissn") + + +class IssueViewSet(SnippetViewSet): + model = Issue + add_view_class = OwnedCreateView + menu_label = _("Issues") + menu_icon = "issue" + menu_order = 30 + add_to_admin_menu = False + list_display = ("journal", "volume", "number", "year") + search_fields = ("journal__title", "volume", "number", "year") + list_filter = ("journal", "year") + + +class JournalManagerViewSetGroup(SnippetViewSetGroup): + menu_name = "journals" + menu_label = _("Journal Manager") + menu_icon = "journal" + menu_order = get_menu_order("journals") + items = (JournalViewSet, IssueViewSet) + + +register_snippet(JournalManagerViewSetGroup) + + +@hooks.register("register_icons") +def register_journals_icons(icons): + return icons + [ + "wagtailadmin/icons/journal.svg", + "wagtailadmin/icons/issue.svg", + ] diff --git a/labeling/__init__.py b/labeling/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/labeling/apps.py b/labeling/apps.py new file mode 100644 index 0000000..3a5256d --- /dev/null +++ b/labeling/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class LabelingConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "labeling" + verbose_name = "Labeling" diff --git a/labeling/citations.py b/labeling/citations.py new file mode 100644 index 0000000..d4be23a --- /dev/null +++ b/labeling/citations.py @@ -0,0 +1,231 @@ +import re + +_PREPOSITIONS = r"(?:de|del|la|los|las|da|do|dos|das|van|von)" + +_SURNAME_RE = rf"[A-ZÁÉÍÓÚÑÇÃÕÂÊÎÔÛ][a-záéíóúñçãõâêîôû]+(?:[-‐\s]+(?:{_PREPOSITIONS})?\s*[A-ZÁÉÍÓÚÑÇÃÕÂÊÎÔÛ]?[a-záéíóúñçãõâêîôû]+)*" + +_PREPOSITIONS_TO_AVOID = [ + "de", "del", "la", "los", "las", + "da", "do", "dos", "das", + "van", "von", +] + + +def find_refid_by_surname_and_date(data_back, searched_surname, searched_date): + for block in data_back: + if block["type"] != "ref_paragraph": + continue + data = block["value"] + + if str(data.get("date")) != str(searched_date[:4]): + continue + + authors = data.get("authors", []) + searched_surnames = searched_surname.split(",") + + for surname in searched_surnames: + if " y " in surname or " and " in surname or " e " in surname or " & " in surname: + if " y " in surname: + surname1 = surname.split(" y ")[0].strip().lower() + surname2 = surname.split(" y ")[1].strip().lower() + if " and " in surname: + surname1 = surname.split(" and ")[0].strip().lower() + surname2 = surname.split(" and ")[1].strip().lower() + if " & " in surname: + surname1 = surname.split(" & ")[0].strip().lower() + surname2 = surname.split(" & ")[1].strip().lower() + if " e " in surname: + surname1 = surname.split(" e ")[0].strip().lower() + surname2 = surname.split(" e ")[1].strip().lower() + + for author_block in authors: + if author_block["type"] == "Author": + author_data = author_block["value"] + if ( + surname1 + in (author_data.get("surname") or "").lower() + + " " + + (author_data.get("given_names") or "").lower() + ): + for author_block2 in authors: + if author_block2["type"] == "Author": + author_data = author_block2["value"] + if ( + surname2 + in (author_data.get("surname") or "").lower() + + " " + + (author_data.get("given_names") or "").lower() + ): + return data.get("refid") + + for author_block in authors: + if author_block["type"] == "Author": + author_data = author_block["value"] + if ( + surname.strip().lower() + in (author_data.get("surname") or "").lower() + + " " + + (author_data.get("given_names") or "").lower() + ): + return data.get("refid") + + if surname.strip().lower() in (data.get("paragraph") or "").lower(): + return data.get("refid") + + return None + + +def extract_apa_citations(text, data_back): + results_list = [] + + for paren in re.finditer(r"\(([^)]+)\)", text): + full_content = paren.group(1) + + if ";" in full_content: + parts = [part for part in full_content.split(";")] + else: + parts = [full_content] + + parenthetical_authors = [] + + for part in parts: + if not part: + continue + + if re.match(r"^\s*\d{4}[a-z]?\s*$", part): + if parenthetical_authors: + last_author = parenthetical_authors[-1] + refid = find_refid_by_surname_and_date(data_back, last_author, part) + results_list.append({ + "cita": part, + "autor": last_author, + "anio": part, + "refid": refid, + }) + continue + + found = False + + pattern1 = rf"(?P{_SURNAME_RE}(?:\s*,\s*{_SURNAME_RE})*\s*,?\s*&\s*{_SURNAME_RE})\s*,\s*(?P\d{{4}}[a-z]?)" + match = re.search(pattern1, part) + if match: + full_authors = match.group("autores") + year = match.group("anio") + first_author = re.split(r"\s*,\s*", full_authors)[0] + parenthetical_authors.append(first_author) + refid = find_refid_by_surname_and_date(data_back, first_author, year) + results_list.append({"cita": part, "autor": first_author, "anio": year, "refid": refid}) + found = True + + if not found: + pattern2 = rf"(?P{_SURNAME_RE}(?:\s*,\s*{_SURNAME_RE})*\s*,?\s*&\s*{_SURNAME_RE})\s+(?P\d{{4}}[a-z]?)" + match = re.search(pattern2, part) + if match: + full_authors = match.group("autores") + year = match.group("anio") + first_author = re.split(r"\s*,\s*", full_authors)[0] + parenthetical_authors.append(first_author) + refid = find_refid_by_surname_and_date(data_back, first_author, year) + results_list.append({"cita": part, "autor": first_author, "anio": year, "refid": refid}) + found = True + + if not found: + pattern3 = rf"(?P{_SURNAME_RE})\s*&\s*(?P{_SURNAME_RE})\s*,\s*(?P\d{{4}}[a-z]?)" + match = re.search(pattern3, part) + if match: + first_author = match.group("autor1") + year = match.group("anio") + parenthetical_authors.append(first_author) + refid = find_refid_by_surname_and_date(data_back, first_author, year) + results_list.append({"cita": part, "autor": first_author, "anio": year, "refid": refid}) + found = True + + if not found: + pattern4 = rf"(?P{_SURNAME_RE})\s+et\s+al\s*\.?\s*,\s*(?P\d{{4}}[a-z]?)" + match = re.search(pattern4, part) + if match: + author = match.group("autor") + year = match.group("anio") + parenthetical_authors.append(author) + refid = find_refid_by_surname_and_date(data_back, author, year) + results_list.append({"cita": part, "autor": author, "anio": year, "refid": refid}) + found = True + + if not found: + pattern5 = rf"(?P{_SURNAME_RE})\s+et\s+al\s*\.?\s+(?P\d{{4}}[a-z]?)" + match = re.search(pattern5, part) + if match: + author = match.group("autor") + year = match.group("anio") + parenthetical_authors.append(author) + refid = find_refid_by_surname_and_date(data_back, author, year) + results_list.append({"cita": part, "autor": author, "anio": year, "refid": refid}) + found = True + + if not found: + pattern6 = rf"(?P{_SURNAME_RE}(?:\s*,\s*{_SURNAME_RE}){{2,}})\s*,\s*(?P\d{{4}}[a-z]?)" + match = re.search(pattern6, part) + if match: + full_authors = match.group("autores") + year = match.group("anio") + first_author = re.split(r"\s*,\s*", full_authors)[0] + parenthetical_authors.append(first_author) + refid = find_refid_by_surname_and_date(data_back, first_author, year) + results_list.append({"cita": part, "autor": first_author, "anio": year, "refid": refid}) + found = True + + if not found: + pattern7 = rf"(?P{_SURNAME_RE})\s*,\s*(?P\d{{4}}[a-z]?)" + match = re.search(pattern7, part) + if match: + author = match.group("autor") + year = match.group("anio") + parenthetical_authors.append(author) + refid = find_refid_by_surname_and_date(data_back, author, year) + results_list.append({"cita": part, "autor": author, "anio": year, "refid": refid}) + found = True + + pattern_multi_year = rf"(?P{_SURNAME_RE})(?:\s*[-‐]\s*{_SURNAME_RE})*(?:\s+et\s+al\s*\.?|\s+(?:y|and|&)\s+{_SURNAME_RE})?\s*\(\s*(?P\d{{4}}[a-z]?(?:\s*,\s*\d{{4}}[a-z]?)+)\s*\)" + + for match in re.finditer(pattern_multi_year, text): + match_start = match.start() + previous_text = text[:match_start].split() + if previous_text and previous_text[-1].lower() in _PREPOSITIONS_TO_AVOID: + continue + + author = match.group("autor") + years_str = match.group("años") + years = [y for y in years_str.split(",")] + + for y in years: + refid = find_refid_by_surname_and_date(data_back, author, y) + results_list.append({ + "cita": f"{author} et al. ({y})" if "et al" in match.group(0) else f"{author} ({y})", + "autor": author, + "anio": y, + "refid": refid, + }) + + pattern_outside = rf"(?P{_SURNAME_RE})(?:\s*[-‐]\s*{_SURNAME_RE})*(?:\s+et\s+al\s*\.?|\s+(?:y|and|&)\s+{_SURNAME_RE})?\s*\(\s*(?P\d{{4}}[a-z]?)\s*\)" + + for match in re.finditer(pattern_outside, text): + match_start = match.start() + previous_text = text[:match_start].split() + if previous_text and previous_text[-1].lower() in _PREPOSITIONS_TO_AVOID: + continue + + author = match.group("autor") + year = match.group("anio") + full_citation = match.group(0) + + is_multiple = False + for result in results_list: + if result["autor"] == author and result["anio"] == year and "," in full_citation: + is_multiple = True + break + + if not is_multiple: + refid = find_refid_by_surname_and_date(data_back, author, year) + results_list.append({"cita": full_citation, "autor": author, "anio": year, "refid": refid}) + + return results_list diff --git a/labeling/fragments.py b/labeling/fragments.py new file mode 100644 index 0000000..40aaaf5 --- /dev/null +++ b/labeling/fragments.py @@ -0,0 +1,317 @@ +import html +import re + +from lxml import etree + + +class StreamBlockAdapter: + __slots__ = ("block_type", "value") + + def __init__(self, block_type, value): + self.block_type = block_type + self.value = value + + +INLINE_XML_TAG_NAMES = ( + "italic", "xref", "bold", "sub", "sup", + "underline", "sc", "strike", "br", +) + +_INLINE_TAG_PATTERN = re.compile( + rf"]*)?/?>", + re.IGNORECASE, +) + +_TABLE_CELL_PATTERN = re.compile( + r"(]*)?>)(.*?)()", + re.DOTALL | re.IGNORECASE, +) + + +def extract_subsection(text): + text = text.strip() + + match = re.match(r"(?i)\s*(.+?)\s*:\s*(.+)", text) + if match: + label = match.group(1).strip() + content = match.group(2).strip() + else: + label = None + content = text + + return {"title": label, "content": content} + + +def find_special_element_id(data_body, label): + for d in data_body: + if d["type"] in ["image", "table"]: + data = d["value"] + clean_label = re.sub(r"^[\s\.,;:–—-]+", "", label).capitalize() + + if d["type"] == "image": + figlabel = data.get("figlabel") or "" + figid = data.get("figid") or "" + if clean_label == figlabel: + return figid or None + if ( + figid + and len(figid) > 1 + and figid[0] == clean_label.lower()[:1] + and figid[1] in clean_label.lower() + ): + return figid + + if d["type"] == "table": + tablabel = data.get("tablabel") or "" + tabid = data.get("tabid") or "" + if clean_label == tablabel: + return tabid or None + if ( + tabid + and len(tabid) > 1 + and tabid[0] == clean_label.lower()[:1] + and tabid[1] in clean_label.lower() + ): + return tabid + + for d in data_body: + if d["type"] in ["compound_paragraph"]: + data = d["value"] + clean_label = re.sub(r"^[\s\.,;:–—-]+", "", label).lower() + + if d["type"] == "compound_paragraph": + if data["eid"][0] in clean_label[0] and data["eid"][1] in clean_label: + return data.get("eid") + + return None + + +def is_number_parenthesis(text): + pattern = re.compile(r"^\s*\(\s*(\d+)\s*\)\s*$") + match = pattern.fullmatch(text) + if match: + return f"({match.group(1)})" + return None + + +def remove_unpaired_tags(text): + pattern = re.compile(r"<(/?)([a-zA-Z0-9]+)(?:\s[^>]*)?>") + + result = [] + stack = [] + + i = 0 + for match in pattern.finditer(text): + is_closing, tag_name = match.groups() + is_closing = bool(is_closing) + + if match.start() > i: + result.append(text[i:match.start()]) + + tag_text = text[match.start():match.end()] + + if not is_closing: + stack.append((tag_name, len(result))) + result.append(tag_text) + else: + if stack and stack[-1][0] == tag_name: + stack.pop() + result.append(tag_text) + + i = match.end() + + if i < len(text): + result.append(text[i:]) + + for tag_name, pos in sorted(stack, reverse=True, key=lambda x: x[1]): + result.pop(pos) + + return "".join(result) + + +def escape_angle_brackets_outside_tags(text): + if not text or "<" not in text: + return text + + parts = [] + pos = 0 + for match in _INLINE_TAG_PATTERN.finditer(text): + if match.start() > pos: + parts.append(text[pos:match.start()].replace("<", "<")) + parts.append(match.group(0)) + pos = match.end() + if pos < len(text): + parts.append(text[pos:].replace("<", "<")) + return "".join(parts) + + +def iter_front_blocks(article_docx, data_front=None): + if data_front: + try: + first = data_front[0] + except (IndexError, TypeError, KeyError): + first = None + if first is not None: + if isinstance(first, dict) and "type" in first and "value" in first: + for item in data_front: + yield StreamBlockAdapter(item["type"], item["value"]) + return + else: + for item in data_front: + if hasattr(item, "block_type"): + yield item + elif isinstance(item, tuple) and len(item) == 2: + yield StreamBlockAdapter(item[0], item[1]) + else: + yield item + return + for block in article_docx.content: + yield block + + +def plain_paragraph_text(paragraph): + if not paragraph: + return "" + text = str(paragraph) + text = re.sub(r"(?i)", "", text) + text = re.sub(r"\[\s*/?\s*\w+(?:\s+[^\]]+)?\s*\]", "", text) + return re.sub(r"\s+", " ", text).strip() + + +def normalize_aff_ids(affid): + if affid is None or affid == "": + return [] + + items = affid if isinstance(affid, list) else [affid] + result = [] + for item in items: + if item is None or item == "": + continue + if isinstance(item, int): + result.append(item) + continue + if isinstance(item, str) and item.isdigit(): + result.append(int(item)) + continue + digits = re.sub(r"\D", "", str(item)) + if digits: + result.append(int(digits)) + return result + + +def _escape_table_cell_content(inner): + if not inner: + return inner + if "&" in inner or "<" in inner or ">" in inner or "<" in inner: + inner = re.sub(r"&(?!\w+;|#\d+;)", "&", inner) + if "<" in inner: + inner = escape_angle_brackets_outside_tags(inner) + return inner + return html.escape(inner, quote=False) + + +def sanitize_table_html_fragment(table_html): + if not table_html: + return table_html + table_html = re.sub(r"&(?!\w+;|#\d+;)", "&", table_html) + + def fix_cell(match): + return match.group(1) + _escape_table_cell_content(match.group(2)) + match.group(3) + + return _TABLE_CELL_PATTERN.sub(fix_cell, table_html) + + +def sanitize_inline_xml_fragment(fragment): + if not fragment: + return fragment + fragment = re.sub(r"&(?!\w+;|#\d+;)", "&", fragment) + return escape_angle_brackets_outside_tags(fragment) + + +def parse_xml_fragment(fragment): + return etree.XML(sanitize_table_html_fragment(fragment)) + + +def append_fragment(node_dest, val): + if not val: + parent = node_dest.getparent() + if parent: + parent.remove(node_dest) + return + + clean = re.sub(r"(?i)", "", val) + clean = clean.replace("\n", "") + clean = clean.replace(" ", " ") + clean = re.sub(r"&(?!\w+;|#\d+;)", "&", clean) + clean = escape_angle_brackets_outside_tags(clean) + clean = remove_unpaired_tags(clean) + clean = re.sub(r'<(?![/a-zA-Z_])', '<', clean) + + if clean == "": + parent = node_dest.getparent() + if parent: + parent.remove(node_dest) + return + + if "<" not in clean: + node_dest.text = (node_dest.text or "") + clean + return + + wrapper = etree.XML(f"<_wrap_>{clean}") + + if wrapper.text: + node_dest.text = (node_dest.text or "") + wrapper.text + + for child in list(wrapper): + node_dest.append(child) + + +def extract_label_and_title(text): + pattern = ( + r"\b(Imagen|Imágen|Image|Imagem|Figura|Figure|Tabla|Table|Tabela)\s+(\d+)\b" + ) + match = re.search(pattern, text, re.IGNORECASE) + + if match: + word = match.group(1).capitalize() + number = match.group(2) + label = f"{word} {number}" + rest = text[match.end():] + rest_clean = re.sub(r"^[\s\.,;:–—-]+", "", rest) + return {"label": label, "title": rest_clean.strip()} + + return {"label": None, "title": text.strip()} + + +def process_special_content(text, data_body): + text = re.sub(r"[\u00A0\u2007\u202F]", " ", text) + + pattern = r""" + (?]+>", "", text) + + +def map_text(text): + label_map = {} + pattern = r"<[^>]+>.*?]+>|<[^/>]+/>" + matches = re.findall(pattern, text, re.DOTALL) + for match in matches: + clean_content = clean_labels(match) + if clean_content: + label_map[clean_content] = match + return label_map + + +def find_positions(text, substring): + positions = [] + start = 0 + while True: + pos = text.find(substring, start) + if pos == -1: + break + positions.append((pos, pos + len(substring))) + start = pos + 1 + return positions + + +def extract_labels(original_text, clean_text, start_pos, end_pos): + clean_char_count = 0 + result = "" + in_range = False + + i = 0 + while i < len(original_text) and clean_char_count <= end_pos: + char = original_text[i] + + if char == "<": + tag_end = original_text.find(">", i) + if tag_end != -1: + tag = original_text[i:tag_end + 1] + if in_range: + result += tag + i = tag_end + 1 + continue + + if clean_char_count == start_pos: + in_range = True + elif clean_char_count == end_pos: + in_range = False + break + + if in_range: + result += char + + clean_char_count += 1 + i += 1 + + return result + + +def restore_labels_ref(ref, label_map, original_text, clean_text): + positions = find_positions(clean_text, ref) + if not positions: + return ref + + candidates = [] + for start_pos, end_pos in positions: + original_fragment = extract_labels(original_text, clean_text, start_pos, end_pos) + if original_fragment != ref: + candidates.append(original_fragment) + + return candidates[0] if candidates else ref + + +def process_labeled_text(text, data_back): + transform_map = map_text(text) + clean_text = clean_labels(text) + refs = extract_apa_citations(clean_text, data_back) + + labeled_refs = [] + for ref in refs: + restored = dict(ref) + restored["cita"] = restore_labels_ref(ref["cita"], transform_map, text, clean_text) + labeled_refs.append(restored) + + return labeled_refs + + +def match_by_regex(text, order_labels): + return next( + ( + key_obj + for key_obj in order_labels.items() + if "regex" in key_obj[1] and re.search(key_obj[1]["regex"], text) + ), + None, + ) + + +def match_by_style_and_size(item, order_labels, style="bold"): + return next( + ( + key_obj + for key_obj in order_labels.items() + if "size" in key_obj[1] + and style in key_obj[1] + and key_obj[1]["size"] == item.get("font_size") + and key_obj[1][style] == item.get(style) + ), + None, + ) + + +def match_next_label(item, label_next, order_labels): + return next( + ( + key_obj + for key_obj in order_labels.items() + if "size" in key_obj[1] + and key_obj[1]["size"] == item.get("font_size") + and key_obj[0] == label_next + ), + None, + ) + + +def match_paragraph(item, order_labels): + return next( + ( + key_obj + for key_obj in order_labels.items() + if "size" in key_obj[1] + and "next" in key_obj[1] + and key_obj[1]["size"] == item.get("font_size") + and key_obj[1]["next"] == "

" + ), + None, + ) + + +def match_section(item, sections): + return ( + {"label": "", "body": True} + if ( + item.get("font_size") == sections[0].get("size") + and item.get("bold") == sections[0].get("bold") + and item.get("text", "").isupper() == sections[0].get("isupper") + ) + else None + ) + + +def match_subsection(item, sections): + return ( + {"label": "", "body": True} + if ( + item.get("font_size") == sections[1].get("size") + and item.get("bold") == sections[1].get("bold") + and item.get("text", "").isupper() == sections[1].get("isupper") + ) + else None + ) + + +def _looks_like_frontmatter(text): + if not text or not text.strip(): + return False + for pattern in _BODY_START_PATTERNS: + if pattern.search(text): + return False + for pattern in _FRONTMATTER_PATTERNS: + if pattern.search(text): + return True + return False + + +def create_labeled_object(i, item, state, sections): + obj = {} + result = None + + if match_section(item, sections): + result = match_section(item, sections) + state["label"] = result.get("label") + state["body"] = result.get("body") + + if match_subsection(item, sections): + result = match_subsection(item, sections) + state["label"] = result.get("label") + state["body"] = result.get("body") + + if ( + state.get("body") + and re.search(r"^(refer)", item.get("text").lower()) + and match_section(item, sections) + ): + state["label"] = "" + state["body"] = False + state["back"] = True + obj["type"] = "paragraph" + obj["value"] = {"label": state["label"], "paragraph": item.get("text")} + + if state.get("body") and re.search( + r"^(refer[eê]nci|references?)\s*$", item.get("text").strip().lower() + ): + state["label"] = "" + state["body"] = False + state["back"] = True + result = {"label": "", "body": False, "back": True} + obj["type"] = "paragraph" + obj["value"] = {"label": state["label"], "paragraph": item.get("text")} + + if not result: + result = {"label": "

", "body": state["body"], "back": state["back"]} + state["label"] = result.get("label") + state["body"] = result.get("body") + state["back"] = result.get("back") + + if not result: + if state.get("label_next"): + if state.get("repeat"): + result = match_by_regex(item.get("text"), order_labels) + if result: + state["label"] = result[0] + else: + result = match_by_style_and_size(item, order_labels, style="bold") + if result: + state["label"] = result[0] + state["repeat"] = None + state["reset"] = None + state["label_next"] = result[1].get("next") + state["body"] = result[1].get("size") == 16 + if state["body"] and re.search(r"^(refer)", item.get("text").lower()): + state["body"] = False + state["back"] = True + if not result: + result = match_next_label(item, state["label_next"], order_labels) + if result: + state["label"] = result[0] + state["label_next_reset"] = result[1].get("next") + state["reset"] = result[1].get("reset", False) + state["repeat"] = result[1].get("repeat", False) + else: + result = match_by_style_and_size(item, order_labels, style="bold") + if result: + state["label"] = result[0] + state["label_next"] = result[1].get("next") + if state.get("body") and re.search(r"^(refer)", item.get("text").lower()): + state["body"] = False + state["back"] = True + else: + result = match_by_style_and_size(item, order_labels, style="italic") + if result: + state["label"] = re.sub(r"-\d+", "", result[0]) + state["label_next"] = result[1].get("next") + else: + result = match_by_regex(item.get("text"), order_labels) + if result: + state["label"] = result[0] + else: + result = match_paragraph(item, order_labels) + if result: + state["label"] = result[0] + + if result: + obj["type"] = "paragraph" + obj["value"] = {"label": state["label"], "paragraph": item.get("text")} + + if state["label"] == "": + obj["type"] = "author_paragraph" + elif state["label"] == "": + obj["type"] = "aff_paragraph" + + if re.search(r"^(translation)", item.get("text").lower()): + state["label"] = "" + state["body"] = False + state["back"] = False + obj["type"] = "paragraph_with_language" + obj["value"] = {"label": state["label"], "paragraph": item.get("text")} + + if state.get("body") and _looks_like_frontmatter(item.get("text", "")): + state["body"] = False + state["back"] = False + + return obj, result, state + + +def create_special_content_object(item, stream_data_body, counts): + obj = {} + + if item.get("type") == "image": + obj = {} + counts["numfig"] += 1 + obj["type"] = "image" + obj["value"] = { + "figid": f"f{counts['numfig']}", + "label": "", + "image": item.get("image"), + } + + try: + prev_element = stream_data_body[-1] + label_title = extract_label_and_title(prev_element["value"]["paragraph"]) + obj["value"]["figlabel"] = label_title["label"] + obj["value"]["title"] = label_title["title"] + stream_data_body.pop(-1) + except Exception: + pass + + elif item.get("type") == "table": + obj = {} + counts["numtab"] += 1 + obj["type"] = "table" + obj["value"] = { + "tabid": f"t{counts['numtab']}", + "label": "", + "content": item.get("table"), + } + + try: + prev_element = stream_data_body[-1] + label_title = extract_label_and_title(prev_element["value"]["paragraph"]) + obj["value"]["tablabel"] = label_title["label"] + obj["value"]["title"] = label_title["title"] + stream_data_body.pop(-1) + except Exception: + pass + + elif item.get("type") == "list": + obj = {} + obj["type"] = "paragraph" + obj["value"] = {"label": "", "paragraph": item.get("list")} + + elif item.get("type") == "compound": + obj = {} + counts["numeq"] += 1 + obj["type"] = "compound_paragraph" + obj["value"] = { + "eid": f"e{counts['numeq']}", + "content": item.get("text"), + } + text_count = sum(1 for c in obj["value"]["content"] if c["type"] == "text") + + if text_count > 1: + obj["value"]["label"] = "" + return obj, counts + + if text_count == 0: + obj["value"]["label"] = "" + return obj, counts + + text_value = next( + item["value"] for item in obj["value"]["content"] if item["type"] == "text" + ) + text = is_number_parenthesis(text_value) + if text: + obj["value"]["label"] = "" + next(item for item in obj["value"]["content"] if item["type"] == "text")["value"] = text + else: + obj["value"]["label"] = "" + + return obj, counts diff --git a/local.yml b/local.yml new file mode 100644 index 0000000..b203206 --- /dev/null +++ b/local.yml @@ -0,0 +1,78 @@ +services: + django: &django + build: + context: . + dockerfile: ${DOCKERFILE:-./compose/local/django/Dockerfile} + args: + BUILD_ENVIRONMENT: local + image: scielo_tools_local_django + container_name: scielo_tools_local_django + depends_on: + - redis + - postgres + - mailhog + volumes: + - .:/app:z + # - ../packtools:/packtools:z + env_file: + - ./.envs/.local/.django + - ./.envs/.local/.postgres + ports: + - "8009:8000" + command: /start + + mailhog: + image: mailhog/mailhog:v1.0.0 + container_name: scielo_tools_local_mailhog + ports: + - "8029:8025" + + postgres: + build: + context: . + dockerfile: ./compose/production/postgres/Dockerfile + image: scielo_tools_local_postgres + container_name: scielo_tools_local_postgres + volumes: + - ../scms_data/scielo_tools/data_dev:/var/lib/postgresql:Z + - ../scms_data/scielo_tools/data_dev_backup:/backups:z + ports: + - "5439:5432" + env_file: + - ./.envs/.local/.postgres + + redis: + image: redis:8 + container_name: scielo_tools_local_redis + ports: + - "6399:6379" + + celeryworker: + <<: *django + image: scielo_tools_local_celeryworker + container_name: scielo_tools_local_celeryworker + depends_on: + - redis + - postgres + - mailhog + ports: [] + command: /start-celeryworker + + celerybeat: + <<: *django + image: scielo_tools_local_celerybeat + container_name: scielo_tools_local_celerybeat + depends_on: + - redis + - postgres + - mailhog + ports: [] + command: /start-celerybeat + + flower: + <<: *django + image: scielo_tools_local_flower + container_name: scielo_tools_local_flower + ports: + - "5559:5555" + command: /start-flower diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po new file mode 100755 index 0000000..b7e2885 --- /dev/null +++ b/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,1413 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-10-24 13:56+0000\n" +"PO-Revision-Date: 2025-10-27 HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: English \n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: core/choices.py:192 +msgid "Editor-Chefe" +msgstr "Editor-in-Chief" + +#: core/choices.py:193 +msgid "Editor(es) Executivo" +msgstr "Executive Editor(s)" + +#: core/choices.py:194 +msgid "Editor(es) Associados ou de Seção" +msgstr "Associate or Section Editor(s)" + +#: core/choices.py:195 +msgid "Equipe Técnica" +msgstr "Technical Team" + +#: core/choices.py:199 +msgid "January" +msgstr "January" + +#: core/choices.py:200 +msgid "February" +msgstr "February" + +#: core/choices.py:201 +msgid "March" +msgstr "March" + +#: core/choices.py:202 +msgid "April" +msgstr "April" + +#: core/choices.py:203 +msgid "May" +msgstr "May" + +#: core/choices.py:204 +msgid "June" +msgstr "June" + +#: core/choices.py:205 +msgid "July" +msgstr "July" + +#: core/choices.py:206 +msgid "August" +msgstr "August" + +#: core/choices.py:207 +msgid "September" +msgstr "September" + +#: core/choices.py:208 +msgid "October" +msgstr "October" + +#: core/choices.py:209 +msgid "November" +msgstr "November" + +#: core/choices.py:210 +msgid "December" +msgstr "December" + +#: core/choices.py:216 +msgid "by" +msgstr "by" + +#: core/choices.py:217 +msgid "by-sa" +msgstr "by-sa" + +#: core/choices.py:218 +msgid "by-nc" +msgstr "by-nc" + +#: core/choices.py:219 +msgid "by-nc-sa" +msgstr "by-nc-sa" + +#: core/choices.py:220 +msgid "by-nd" +msgstr "by-nd" + +#: core/choices.py:221 +msgid "by-nc-nd" +msgstr "by-nc-nd" + +#: core/choices.py:225 +msgid "Male" +msgstr "Male" + +#: core/choices.py:226 +msgid "Female" +msgstr "Female" + +#: core/home/templates/home/welcome_page.html:6 +msgid "Visit the Wagtail website" +msgstr "Visit the Wagtail website" + +#: core/home/templates/home/welcome_page.html:15 +msgid "View the release notes" +msgstr "View the release notes" + +#: core/home/templates/home/welcome_page.html:27 +msgid "Welcome to your new Wagtail site!" +msgstr "Welcome to your new Wagtail site!" + +#: core/home/templates/home/welcome_page.html:35 +msgid "Wagtail Documentation" +msgstr "Wagtail Documentation" + +#: core/home/templates/home/welcome_page.html:36 +msgid "Topics, references, & how-tos" +msgstr "Topics, references, & how-tos" + +#: core/home/templates/home/welcome_page.html:42 +msgid "Tutorial" +msgstr "Tutorial" + +#: core/home/templates/home/welcome_page.html:43 +msgid "Build your first Wagtail site" +msgstr "Build your first Wagtail site" + +#: core/home/templates/home/welcome_page.html:49 +msgid "Admin Interface" +msgstr "Admin Interface" + +#: core/home/templates/home/welcome_page.html:50 +msgid "Create your superuser first!" +msgstr "Create your superuser first!" + +#: core/models.py:28 tracker/models.py:76 +msgid "Creation date" +msgstr "Creation date" + +#: core/models.py:31 +msgid "Last update date" +msgstr "Last update date" + +#: core/models.py:36 +msgid "Creator" +msgstr "Creator" + +#: core/models.py:46 +msgid "Updater" +msgstr "Updater" + +#: core/models.py:66 +msgid "Code" +msgstr "Code" + +#: core/models.py:68 +msgid "Sex" +msgstr "Sex" + +#: core/models.py:133 +msgid "Language Name" +msgstr "Language Name" + +#: core/models.py:134 +msgid "Language code 2" +msgstr "Language code 2" + +#: core/models.py:142 core/models.py:190 core/models.py:207 core/models.py:251 +#: core/models.py:523 markup_doc/models.py:357 xml_manager/models.py:58 +#: xml_manager/models.py:90 +msgid "Language" +msgstr "Language" + +#: core/models.py:143 +msgid "Languages" +msgstr "Languages" + +#: core/models.py:186 markup_doc/models.py:129 +msgid "Text" +msgstr "Text" + +#: core/models.py:202 core/models.py:247 +msgid "Rich Text" +msgstr "Rich Text" + +#: core/models.py:203 +msgid "Plain Text" +msgstr "Plain Text" + +#: core/models.py:268 +msgid "Year" +msgstr "Year" + +#: core/models.py:269 +msgid "Month" +msgstr "Month" + +#: core/models.py:270 django_celery_beat/choices.py:18 +msgid "Day" +msgstr "Day" + +#: core/models.py:301 core/models.py:382 +msgid "License" +msgstr "License" + +#: core/models.py:302 core/models.py:383 +msgid "Licenses" +msgstr "Licenses" + +#: core/models.py:515 +msgid "File" +msgstr "File" + +#: core_settings/models.py:18 core_settings/models.py:19 +msgid "Site configuration" +msgstr "Site configuration" + +#: core_settings/models.py:66 +msgid "Site settings" +msgstr "Site settings" + +#: core_settings/models.py:67 +msgid "Admin settings" +msgstr "Admin settings" + +#: django_celery_beat/admin.py:69 django_celery_beat/forms.py:55 +msgid "Task (registered)" +msgstr "Task (registered)" + +#: django_celery_beat/admin.py:73 django_celery_beat/forms.py:59 +msgid "Task (custom)" +msgstr "Task (custom)" + +#: django_celery_beat/admin.py:90 django_celery_beat/forms.py:75 +msgid "Need name of task" +msgstr "Need name of task" + +#: django_celery_beat/admin.py:96 django_celery_beat/forms.py:81 +#: django_celery_beat/models.py:648 +msgid "Only one can be set, in expires and expire_seconds" +msgstr "Only one can be set, in expires and expire_seconds" + +#: django_celery_beat/admin.py:106 django_celery_beat/forms.py:91 +#, python-format +msgid "Unable to parse JSON: %s" +msgstr "Unable to parse JSON: %s" + +#: django_celery_beat/admin.py:207 django_celery_beat/wagtail_hooks.py:70 +#, python-brace-format +msgid "{0} task{1} {2} successfully {3}" +msgstr "{0} task{1} {2} successfully {3}" + +#: django_celery_beat/admin.py:210 django_celery_beat/admin.py:283 +#: django_celery_beat/wagtail_hooks.py:73 +#: django_celery_beat/wagtail_hooks.py:154 +msgid "was,were" +msgstr "was,were" + +#: django_celery_beat/admin.py:220 django_celery_beat/wagtail_hooks.py:83 +msgid "Enable selected tasks" +msgstr "Enable selected tasks" + +#: django_celery_beat/admin.py:227 django_celery_beat/wagtail_hooks.py:90 +msgid "Disable selected tasks" +msgstr "Disable selected tasks" + +#: django_celery_beat/admin.py:242 django_celery_beat/wagtail_hooks.py:105 +msgid "Toggle activity of selected tasks" +msgstr "Toggle activity of selected tasks" + +#: django_celery_beat/admin.py:266 django_celery_beat/wagtail_hooks.py:130 +#, python-brace-format +msgid "task \"{0}\" not found" +msgstr "task \"{0}\" not found" + +#: django_celery_beat/admin.py:280 django_celery_beat/wagtail_hooks.py:151 +#, python-brace-format +msgid "{0} task{1} {2} successfully run" +msgstr "{0} task{1} {2} successfully run" + +#: django_celery_beat/admin.py:287 django_celery_beat/wagtail_hooks.py:158 +msgid "Run selected tasks" +msgstr "Run selected tasks" + +#: django_celery_beat/apps.py:13 +msgid "Periodic Tasks" +msgstr "Periodic Tasks" + +#: django_celery_beat/button_helper.py:15 +msgid "Run" +msgstr "Run" + +#: django_celery_beat/choices.py:10 +msgid "Days" +msgstr "Days" + +#: django_celery_beat/choices.py:11 +msgid "Hours" +msgstr "Hours" + +#: django_celery_beat/choices.py:12 +msgid "Minutes" +msgstr "Minutes" + +#: django_celery_beat/choices.py:13 +msgid "Seconds" +msgstr "Seconds" + +#: django_celery_beat/choices.py:14 +msgid "Microseconds" +msgstr "Microseconds" + +#: django_celery_beat/choices.py:19 +msgid "Hour" +msgstr "Hour" + +#: django_celery_beat/choices.py:20 +msgid "Minute" +msgstr "Minute" + +#: django_celery_beat/choices.py:21 +msgid "Second" +msgstr "Second" + +#: django_celery_beat/choices.py:22 +msgid "Microsecond" +msgstr "Microsecond" + +#: django_celery_beat/choices.py:26 +msgid "Astronomical dawn" +msgstr "Astronomical dawn" + +#: django_celery_beat/choices.py:27 +msgid "Civil dawn" +msgstr "Civil dawn" + +#: django_celery_beat/choices.py:28 +msgid "Nautical dawn" +msgstr "Nautical dawn" + +#: django_celery_beat/choices.py:29 +msgid "Astronomical dusk" +msgstr "Astronomical dusk" + +#: django_celery_beat/choices.py:30 +msgid "Civil dusk" +msgstr "Civil dusk" + +#: django_celery_beat/choices.py:31 +msgid "Nautical dusk" +msgstr "Nautical dusk" + +#: django_celery_beat/choices.py:32 +msgid "Solar noon" +msgstr "Solar noon" + +#: django_celery_beat/choices.py:33 +msgid "Sunrise" +msgstr "Sunrise" + +#: django_celery_beat/choices.py:34 +msgid "Sunset" +msgstr "Sunset" + +#: django_celery_beat/models.py:70 +msgid "Solar Event" +msgstr "Solar Event" + +#: django_celery_beat/models.py:71 +msgid "The type of solar event when the job should run" +msgstr "The type of solar event when the job should run" + +#: django_celery_beat/models.py:76 +msgid "Latitude" +msgstr "Latitude" + +#: django_celery_beat/models.py:77 +msgid "Run the task when the event happens at this latitude" +msgstr "Run the task when the event happens at this latitude" + +#: django_celery_beat/models.py:83 +msgid "Longitude" +msgstr "Longitude" + +#: django_celery_beat/models.py:84 +msgid "Run the task when the event happens at this longitude" +msgstr "Run the task when the event happens at this longitude" + +#: django_celery_beat/models.py:91 +msgid "solar event" +msgstr "solar event" + +#: django_celery_beat/models.py:92 +msgid "solar events" +msgstr "solar events" + +#: django_celery_beat/models.py:132 +msgid "Number of Periods" +msgstr "Number of Periods" + +#: django_celery_beat/models.py:134 +msgid "Number of interval periods to wait before running the task again" +msgstr "Number of interval periods to wait before running the task again" + +#: django_celery_beat/models.py:141 +msgid "Interval Period" +msgstr "Interval Period" + +#: django_celery_beat/models.py:142 +msgid "The type of period between task runs (Example: days)" +msgstr "The type of period between task runs (Example: days)" + +#: django_celery_beat/models.py:148 +msgid "interval" +msgstr "interval" + +#: django_celery_beat/models.py:149 +msgid "intervals" +msgstr "intervals" + +#: django_celery_beat/models.py:169 +msgid "Minute (integer from 0-59)" +msgstr "Minute (integer from 0-59)" + +#: django_celery_beat/models.py:171 +msgid "Hour (integer from 0-23)" +msgstr "Hour (integer from 0-23)" + +#: django_celery_beat/models.py:173 +msgid "Day of the week (0-6, 0 is Sunday, or 'mon', 'tue', etc.)" +msgstr "Day of the week (0-6, 0 is Sunday, or 'mon', 'tue', etc.)" + +#: django_celery_beat/models.py:176 +msgid "Day of the month (1-31)" +msgstr "Day of the month (1-31)" + +#: django_celery_beat/models.py:178 +msgid "Month of the year (1-12)" +msgstr "Month of the year (1-12)" + +#: django_celery_beat/models.py:180 +msgid "Timezone" +msgstr "Timezone" + +#: django_celery_beat/models.py:209 +msgid "The human-readable timezone name that this schedule will follow (e.g. 'Europe/Berlin')" +msgstr "The human-readable timezone name that this schedule will follow (e.g. 'Europe/Berlin')" + +#: django_celery_beat/models.py:233 +msgid "crontab" +msgstr "crontab" + +#: django_celery_beat/models.py:234 +msgid "crontabs" +msgstr "crontabs" + +#: django_celery_beat/models.py:254 +msgid "Name" +msgstr "Name" + +#: django_celery_beat/models.py:255 +msgid "Event" +msgstr "Event" + +#: django_celery_beat/models.py:301 +msgid "Clocked Time" +msgstr "Clocked Time" + +#: django_celery_beat/models.py:308 +msgid "clocked schedule" +msgstr "clocked schedule" + +#: django_celery_beat/models.py:309 +msgid "clocked schedules" +msgstr "clocked schedules" + +#: django_celery_beat/models.py:407 +msgid "seconds" +msgstr "seconds" + +#: django_celery_beat/models.py:412 +msgid "positional arguments" +msgstr "positional arguments" + +#: django_celery_beat/models.py:414 +msgid "JSON encoded positional arguments (Example: [\"arg1\", \"arg2\"])" +msgstr "JSON encoded positional arguments (Example: [\"arg1\", \"arg2\"])" + +#: django_celery_beat/models.py:419 +msgid "keyword arguments" +msgstr "keyword arguments" + +#: django_celery_beat/models.py:421 +msgid "JSON encoded keyword arguments (Example: {\"argument\": \"value\"})" +msgstr "JSON encoded keyword arguments (Example: {\"argument\": \"value\"})" + +#: django_celery_beat/models.py:426 +msgid "queue" +msgstr "queue" + +#: django_celery_beat/models.py:430 +msgid "exchange" +msgstr "exchange" + +#: django_celery_beat/models.py:434 +msgid "routing key" +msgstr "routing key" + +#: django_celery_beat/models.py:438 +msgid "headers" +msgstr "headers" + +#: django_celery_beat/models.py:440 +msgid "JSON encoded message headers for the AMQP message." +msgstr "JSON encoded message headers for the AMQP message." + +#: django_celery_beat/models.py:444 +msgid "priority" +msgstr "priority" + +#: django_celery_beat/models.py:446 +msgid "Priority Number between 0 and 255. Supported by: RabbitMQ." +msgstr "Priority Number between 0 and 255. Supported by: RabbitMQ." + +#: django_celery_beat/models.py:453 +msgid "expires" +msgstr "expires" + +#: django_celery_beat/models.py:455 +msgid "Datetime after which the schedule will no longer trigger the task to run" +msgstr "Datetime after which the schedule will no longer trigger the task to run" + +#: django_celery_beat/models.py:460 +msgid "expire seconds" +msgstr "expire seconds" + +#: django_celery_beat/models.py:462 +msgid "Timedelta with seconds which the schedule will no longer trigger the task to run" +msgstr "Timedelta with seconds which the schedule will no longer trigger the task to run" + +#: django_celery_beat/models.py:467 +msgid "one-off task" +msgstr "one-off task" + +#: django_celery_beat/models.py:469 +msgid "If True, the schedule will only run the task a single time" +msgstr "If True, the schedule will only run the task a single time" + +#: django_celery_beat/models.py:473 +msgid "start datetime" +msgstr "start datetime" + +#: django_celery_beat/models.py:475 +msgid "Datetime when the schedule should begin triggering the task to run" +msgstr "Datetime when the schedule should begin triggering the task to run" + +#: django_celery_beat/models.py:480 +msgid "enabled" +msgstr "enabled" + +#: django_celery_beat/models.py:482 +msgid "Set to False to disable the schedule" +msgstr "Set to False to disable the schedule" + +#: django_celery_beat/models.py:486 +msgid "last run at" +msgstr "last run at" + +#: django_celery_beat/models.py:488 +msgid "Datetime that the schedule last triggered the task to run. Reset to None if enabled is set to False." +msgstr "Datetime that the schedule last triggered the task to run. Reset to None if enabled is set to False." + +#: django_celery_beat/models.py:493 +msgid "total run count" +msgstr "total run count" + +#: django_celery_beat/models.py:495 +msgid "Running count of how many times the schedule has triggered the task" +msgstr "Running count of how many times the schedule has triggered the task" + +#: django_celery_beat/models.py:499 +msgid "datetime changed" +msgstr "datetime changed" + +#: django_celery_beat/models.py:501 +msgid "Datetime that this PeriodicTask was last modified" +msgstr "Datetime that this PeriodicTask was last modified" + +#: django_celery_beat/models.py:505 +msgid "description" +msgstr "description" + +#: django_celery_beat/models.py:507 +msgid "Detailed description about the details of this Periodic Task" +msgstr "Detailed description about the details of this Periodic Task" + +#: django_celery_beat/models.py:755 +msgid "Only one schedule can be selected: Interval, Crontab, Solar, or Clocked" +msgstr "Only one schedule can be selected: Interval, Crontab, Solar, or Clocked" + +#: django_celery_beat/models.py:759 +msgid "One of interval, crontab, solar, or clocked must be set." +msgstr "One of interval, crontab, solar, or clocked must be set." + +#: django_celery_beat/models.py:785 +msgid "periodic task" +msgstr "periodic task" + +#: django_celery_beat/models.py:786 +msgid "periodic tasks" +msgstr "periodic tasks" + +#: django_celery_beat/wagtail_hooks.py:217 +msgid "enabled,disabled" +msgstr "enabled,disabled" + +#: django_celery_beat/wagtail_hooks.py:224 +msgid "Periodic tasks" +msgstr "Periodic tasks" + +#: journal/models.py:27 +msgid "Site ID" +msgstr "Site ID" + +#: journal/models.py:29 +msgid "Short title" +msgstr "Short title" + +#: journal/models.py:31 +msgid "Journal ISSN" +msgstr "Journal ISSN" + +#: journal/models.py:33 +msgid "Online Journal ISSN" +msgstr "Online Journal ISSN" + +#: journal/models.py:36 +msgid "ISSN SciELO" +msgstr "ISSN SciELO" + +#: journal/models.py:38 +msgid "ISSN SciELO Legacy" +msgstr "ISSN SciELO Legacy" + +#: journal/models.py:41 +msgid "Subject descriptors" +msgstr "Subject descriptors" + +#: journal/models.py:46 +msgid "Purpose" +msgstr "Purpose" + +#: journal/models.py:49 +msgid "Sponsorship" +msgstr "Sponsorship" + +#: journal/models.py:52 +msgid "Mission" +msgstr "Mission" + +#: journal/models.py:55 +msgid "Index at" +msgstr "Index at" + +#: journal/models.py:58 +msgid "Availability" +msgstr "Availability" + +#: journal/models.py:61 +msgid "Summary form" +msgstr "Summary form" + +#: journal/models.py:64 +msgid "Standard" +msgstr "Standard" + +#: journal/models.py:67 +msgid "Alphabet title" +msgstr "Alphabet title" + +#: journal/models.py:69 +msgid "Print title" +msgstr "Print title" + +#: journal/models.py:72 +msgid "Short title (slug)" +msgstr "Short title (slug)" + +#: journal/models.py:76 +#: markup_doc/models.py:40 +msgid "Title" +msgstr "Title" + +#: journal/models.py:81 +msgid "Subtitle" +msgstr "Subtitle" + +#: journal/models.py:86 +msgid "Next title" +msgstr "Next title" + +#: journal/models.py:90 +msgid "Previous title" +msgstr "Previous title" + +#: journal/models.py:94 +msgid "Control number" +msgstr "Control number" + +#: journal/models.py:97 +msgid "Publisher name" +msgstr "Publisher name" + +#: journal/models.py:99 +msgid "Publisher country" +msgstr "Publisher country" + +#: journal/models.py:102 +msgid "Publisher state" +msgstr "Publisher state" + +#: journal/models.py:105 +msgid "Publisher city" +msgstr "Publisher city" + +#: journal/models.py:108 +msgid "Publisher address" +msgstr "Publisher address" + +#: journal/models.py:111 +msgid "Publication level" +msgstr "Publication level" + +#: journal/models.py:115 +msgid "Email" +msgstr "Email" + +#: journal/models.py:119 +msgid "URL online submission" +msgstr "URL online submission" + +#: journal/models.py:122 +msgid "URL home page" +msgstr "URL home page" + +#: journal/models.py:146 +msgid "Journal" +msgstr "Journal" + +#: journal/models.py:147 +msgid "Journals" +msgstr "Journals" + +#: journal/models.py:162 +msgid "Order" +msgstr "Order" + +#: journal/models.py:174 +msgid "Editor" +msgstr "Editor" + +#: journal/models.py:175 +msgid "Editors" +msgstr "Editors" + +#: journal/wagtail_hooks.py:57 +msgid "Journal and Editor" +msgstr "Journal and Editor" + +#: location/models.py:11 +msgid "Country Name" +msgstr "Country Name" + +#: location/models.py:12 +msgid "ACR3" +msgstr "ACR3" + +#: location/models.py:13 +msgid "ACR2" +msgstr "ACR2" + +#: location/models.py:23 +msgid "Country" +msgstr "Country" + +#: location/models.py:24 +msgid "Countries" +msgstr "Countries" + +#: location/models.py:36 +msgid "State name" +msgstr "State name" + +#: location/models.py:37 +msgid "ACR 2 (state)" +msgstr "ACR 2 (state)" + +#: location/models.py:48 +msgid "State" +msgstr "State" + +#: location/models.py:49 +msgid "States" +msgstr "States" + +#: location/models.py:61 +msgid "City name" +msgstr "City name" + +#: location/models.py:75 +msgid "City" +msgstr "City" + +#: location/models.py:76 +msgid "Cities" +msgstr "Cities" + +#: location/wagtail_hooks.py:55 +msgid "Location" +msgstr "Location" + +#: markup_doc/choices.py:11 +msgid "Document without content" +msgstr "Document without content" + +#: markup_doc/choices.py:12 +msgid "Structured document without references" +msgstr "Structured document without references" + +#: markup_doc/choices.py:13 +msgid "Structured document" +msgstr "Structured document" + +#: markup_doc/choices.py:14 +msgid "Structured document and references" +msgstr "Structured document and references" + +#: markup_doc/models.py:33 +msgid "DOI" +msgstr "DOI" + +#: markup_doc/models.py:56 +msgid "Rich Title" +msgstr "Rich Title" + +#: markup_doc/models.py:58 +msgid "Clean Title" +msgstr "Clean Title" + +#: markup_doc/models.py:97 +msgid "Document" +msgstr "Document" + +#: markup_doc/models.py:98 +#: markup_doc/wagtail_hooks.py:94 +msgid "Documents" +msgstr "Documents" + +#: markup_doc/models.py:130 +msgid "Type" +msgstr "Type" + +#: markup_doc/models.py:137 +msgid "Paragraph" +msgstr "Paragraph" + +#: markup_doc/models.py:138 +msgid "Paragraphs" +msgstr "Paragraphs" + +#: markup_doc/models.py:218 +msgid "Section title" +msgstr "Section title" + +#: markup_doc/models.py:222 +msgid "Section code" +msgstr "Section code" + +#: markup_doc/models.py:245 +msgid "Section" +msgstr "Section" + +#: markup_doc/models.py:246 +msgid "Sections" +msgstr "Sections" + +#: markup_doc/models.py:279 +msgid "Figure" +msgstr "Figure" + +#: markup_doc/models.py:280 +msgid "Figures" +msgstr "Figures" + +#: markup_doc/models.py:306 +msgid "Table" +msgstr "Table" + +#: markup_doc/models.py:307 +msgid "Tables" +msgstr "Tables" + +#: markup_doc/models.py:322 +msgid "Supplementary Material" +msgstr "Supplementary Material" + +#: markup_doc/models.py:323 +msgid "Supplementary Materials" +msgstr "Supplementary Materials" + +#: markup_doc/models.py:336 +msgid "Document ID" +msgstr "Document ID" + +#: markup_doc/models.py:340 +msgid "Document ID type" +msgstr "Document ID type" + +#: markup_doc/models.py:341 +msgid "Publisher ID" +msgstr "Publisher ID" + +#: markup_doc/models.py:342 +msgid "Pub Acronym" +msgstr "Pub Acronym" + +#: markup_doc/models.py:343 +msgid "Vol" +msgstr "Vol" + +#: markup_doc/models.py:344 +msgid "Suppl Vol" +msgstr "Suppl Vol" + +#: markup_doc/models.py:345 +msgid "Num" +msgstr "Num" + +#: markup_doc/models.py:346 +msgid "Suppl Num" +msgstr "Suppl Num" + +#: markup_doc/models.py:346 +msgid "Isid Part" +msgstr "Isid Part" + +#: markup_doc/models.py:347 +msgid "Dateiso" +msgstr "Dateiso" + +#: markup_doc/models.py:348 +msgid "Month/Season" +msgstr "Month/Season" + +#: markup_doc/models.py:349 +msgid "First Page" +msgstr "First Page" + +#: markup_doc/models.py:350 +msgid "@Seq" +msgstr "@Seq" + +#: markup_doc/models.py:351 +msgid "Last Page" +msgstr "Last Page" + +#: markup_doc/models.py:352 +msgid "Elocation ID" +msgstr "Elocation ID" + +#: markup_doc/models.py:353 +msgid "Order (In TOC)" +msgstr "Order (In TOC)" + +#: markup_doc/models.py:354 +msgid "Pag count" +msgstr "Pag count" + +#: markup_doc/models.py:355 +msgid "Doc Topic" +msgstr "Doc Topic" + +#: markup_doc/models.py:363 +msgid "Sps version" +msgstr "Sps version" + +#: markup_doc/models.py:364 +msgid "Artdate" +msgstr "Artdate" + +#: markup_doc/models.py:365 +msgid "Ahpdate" +msgstr "Ahpdate" + +#: markup_doc/models.py:370 +msgid "Document xml" +msgstr "Document xml" + +#: markup_doc/models.py:374 +msgid "Text XML" +msgstr "Text XML" + +#: markup_doc/models.py:513 +msgid "Details" +msgstr "Details" + +#: markup_doc/wagtail_hooks.py:117 +msgid "Documents Markup" +msgstr "Documents Markup" + +#: markup_doc/wagtail_hooks.py:130 +msgid "Carregar DOCX" +msgstr "Load DOCX" + +#: markup_doc/wagtail_hooks.py:148 +msgid "XML marcado" +msgstr "Marked XML" + +#: markup_doc/wagtail_hooks.py:189 +msgid "Modelo de Coleções" +msgstr "Collections Template" + +#: markup_doc/wagtail_hooks.py:209 +msgid "Modelo de Revistas" +msgstr "Journals Template" + +#: markup_doc/wagtail_hooks.py:240 +msgid "DOCX Files" +msgstr "DOCX Files" + +#: model_ai/models.py:21 +msgid "No model" +msgstr "No model" + +#: model_ai/models.py:22 +msgid "Downloading model" +msgstr "Downloading model" + +#: model_ai/models.py:23 +msgid "Model downloaded" +msgstr "Model downloaded" + +#: model_ai/models.py:24 +msgid "Download error" +msgstr "Download error" + +#: model_ai/models.py:34 +msgid "Hugging Face model name" +msgstr "Hugging Face model name" + +#: model_ai/models.py:35 +msgid "Model file" +msgstr "Model file" + +#: model_ai/models.py:36 +msgid "Hugging Face token" +msgstr "Hugging Face token" + +#: model_ai/models.py:38 +msgid "Local model status" +msgstr "Local model status" + +#: model_ai/models.py:44 +msgid "URL Markapi" +msgstr "URL Markapi" + +#: model_ai/models.py:49 +msgid "API KEY Gemini" +msgstr "API KEY Gemini" + +#: model_ai/models.py:67 model_ai/models.py:71 +msgid "Only one instance of LlamaModel is allowed." +msgstr "Only one instance of LlamaModel is allowed." + +#: model_ai/wagtail_hooks.py:28 model_ai/wagtail_hooks.py:63 +msgid "Model name is required." +msgstr "Model name is required." + +#: model_ai/wagtail_hooks.py:32 model_ai/wagtail_hooks.py:67 +msgid "Model file is required." +msgstr "Model file is required." + +#: model_ai/wagtail_hooks.py:36 model_ai/wagtail_hooks.py:71 +msgid "Hugging Face token is required." +msgstr "Hugging Face token is required." + +#: model_ai/wagtail_hooks.py:42 +msgid "Model created and download started." +msgstr "Model created and download started." + +#: model_ai/wagtail_hooks.py:46 model_ai/wagtail_hooks.py:82 +msgid "API AI URL is required." +msgstr "API AI URL is required." + +#: model_ai/wagtail_hooks.py:50 +msgid "Model created, use API AI." +msgstr "Model created, use API AI." + +#: model_ai/wagtail_hooks.py:77 +msgid "Model updated and download started." +msgstr "Model updated and download started." + +#: model_ai/wagtail_hooks.py:79 +msgid "Model updated and already downloaded." +msgstr "Model updated and already downloaded." + +#: model_ai/wagtail_hooks.py:86 +msgid "Model updated, use API AI." +msgstr "Model updated, use API AI." + +#: model_ai/wagtail_hooks.py:95 +msgid "AI LLM Model" +msgstr "AI LLM Model" + +#: reference/models.py:15 +msgid "No reference" +msgstr "No reference" + +#: reference/models.py:16 +msgid "Creating reference" +msgstr "Creating reference" + +#: reference/models.py:17 +msgid "Reference ready" +msgstr "Reference ready" + +#: reference/models.py:22 +msgid "Mixed Citation" +msgstr "Mixed Citation" + +#: reference/models.py:25 +msgid "Reference status" +msgstr "Reference status" + +#: reference/models.py:33 +msgid "Cited Elements" +msgstr "Cited Elements" + +#: reference/models.py:46 +msgid "Marked" +msgstr "Marked" + +#: reference/models.py:47 +msgid "Marked XML" +msgstr "Marked XML" + +#: reference/models.py:56 +msgid "Rating from 1 to 10" +msgstr "Rating from 1 to 10" + +#: reference/wagtail_hooks.py:41 +msgid "Reference" +msgstr "Reference" + +#: tracker/choices.py:9 +msgid "error" +msgstr "error" + +#: tracker/choices.py:10 +msgid "warning" +msgstr "warning" + +#: tracker/choices.py:11 +msgid "info" +msgstr "info" + +#: tracker/choices.py:12 +msgid "exception" +msgstr "exception" + +#: tracker/choices.py:24 +msgid "To reprocess" +msgstr "To reprocess" + +#: tracker/choices.py:25 +msgid "To do" +msgstr "To do" + +#: tracker/choices.py:26 +msgid "Done" +msgstr "Done" + +#: tracker/choices.py:27 +msgid "Doing" +msgstr "Doing" + +#: tracker/choices.py:28 +msgid "Pending" +msgstr "Pending" + +#: tracker/choices.py:29 +msgid "ignored" +msgstr "ignored" + +#: tracker/choices.py:41 +msgid "XML Parsing Error" +msgstr "XML Parsing Error" + +#: tracker/choices.py:42 +msgid "XML Validation Error" +msgstr "XML Validation Error" + +#: tracker/choices.py:43 +msgid "XML Conversion to DOCX Error" +msgstr "XML Conversion to DOCX Error" + +#: tracker/choices.py:44 +msgid "XML Conversion to HTML Error" +msgstr "XML Conversion to HTML Error" + +#: tracker/choices.py:45 +msgid "XML Conversion to PDF Error" +msgstr "XML Conversion to PDF Error" + +#: tracker/choices.py:46 +msgid "XML Conversion to TEX Error" +msgstr "XML Conversion to TEX Error" + +#: tracker/choices.py:47 +msgid "Unknown Error" +msgstr "Unknown Error" + +#: tracker/models.py:28 +msgid "Error Type" +msgstr "Error Type" + +#: tracker/models.py:35 +msgid "Data" +msgstr "Data" + +#: tracker/models.py:39 +msgid "Message" +msgstr "Message" + +#: tracker/models.py:44 +msgid "Handled" +msgstr "Handled" + +#: tracker/models.py:70 +msgid "XML Document Event" +msgstr "XML Document Event" + +#: tracker/models.py:71 tracker/wagtail_hooks.py:16 +msgid "XML Document Events" +msgstr "XML Document Events" + +#: tracker/models.py:77 +msgid "Exception Type" +msgstr "Exception Type" + +#: tracker/models.py:78 +msgid "Exception Msg" +msgstr "Exception Msg" + +#: tracker/models.py:82 +msgid "Item" +msgstr "Item" + +#: tracker/models.py:88 xml_manager/wagtail_hooks.py:69 +msgid "Action" +msgstr "Action" + +#: tracker/models.py:101 +msgid "General Event" +msgstr "General Event" + +#: tracker/models.py:102 tracker/wagtail_hooks.py:47 +msgid "General Events" +msgstr "General Events" + +#: tracker/wagtail_hooks.py:81 +msgid "Unexpected Events" +msgstr "Unexpected Events" + +#: users/admin.py:13 +msgid "Personal info" +msgstr "Personal info" + +#: users/admin.py:15 +msgid "Permissions" +msgstr "Permissions" + +#: users/admin.py:26 +msgid "Important dates" +msgstr "Important dates" + +#: users/forms.py:11 +msgid "This username has already been taken." +msgstr "This username has already been taken." + +#: users/models.py:15 +msgid "Name of User" +msgstr "Name of User" + +#: xml_manager/models.py:9 +msgid "XML File" +msgstr "XML File" + +#: xml_manager/models.py:10 +msgid "Upload an XML file for processing." +msgstr "Upload an XML file for processing." + +#: xml_manager/models.py:16 +msgid "Validation File" +msgstr "Validation File" + +#: xml_manager/models.py:22 +msgid "Exceptions File" +msgstr "Exceptions File" + +#: xml_manager/models.py:26 xml_manager/models.py:61 xml_manager/models.py:93 +msgid "Uploaded At" +msgstr "Uploaded At" + +#: xml_manager/models.py:27 +msgid "The date and time when the file was uploaded." +msgstr "The date and time when the file was uploaded." + +#: xml_manager/models.py:38 xml_manager/models.py:43 xml_manager/models.py:82 +#: xml_manager/wagtail_hooks.py:56 xml_manager/wagtail_hooks.py:60 +msgid "XML Document" +msgstr "XML Document" + +#: xml_manager/models.py:39 xml_manager/wagtail_hooks.py:57 +msgid "XML Documents" +msgstr "XML Documents" + +#: xml_manager/models.py:46 +msgid "PDF File" +msgstr "PDF File" + +#: xml_manager/models.py:50 +msgid "DOCX File" +msgstr "DOCX File" + +#: xml_manager/models.py:53 +msgid "Intermediate DOCX file generated during PDF creation" +msgstr "Intermediate DOCX file generated during PDF creation" + +#: xml_manager/models.py:59 xml_manager/models.py:91 +msgid "Language code or name" +msgstr "Language code or name" + +#: xml_manager/models.py:67 xml_manager/wagtail_hooks.py:77 +#: xml_manager/wagtail_hooks.py:81 +msgid "XML Document PDF" +msgstr "XML Document PDF" + +#: xml_manager/models.py:68 xml_manager/wagtail_hooks.py:78 +msgid "XML Document PDFs" +msgstr "XML Document PDFs" + +#: xml_manager/models.py:85 +msgid "HTML File" +msgstr "HTML File" + +#: xml_manager/models.py:99 xml_manager/wagtail_hooks.py:98 +#: xml_manager/wagtail_hooks.py:102 +msgid "XML Document HTML" +msgstr "XML Document HTML" + +#: xml_manager/models.py:100 xml_manager/wagtail_hooks.py:99 +msgid "XML Document HTMLs" +msgstr "XML Document HTMLs" + +#: xml_manager/wagtail_hooks.py:66 +msgid "Validation file" +msgstr "Validation file" + +#: xml_manager/wagtail_hooks.py:67 +msgid "Exceptions file" +msgstr "Exceptions file" + +#: xml_manager/wagtail_hooks.py:118 +msgid "XML Processor" +msgstr "XML Processor" + + +#: menu items +msgid "XML Files" +msgstr "XML Files" + +#: menu items +msgid "Tarefas" +msgstr "Tasks" + +#: markup_doc/wagtail_hooks.py +msgid "You must first select a collection." +msgstr "You must first select a collection." + +#: markup_doc/wagtail_hooks.py +msgid "Wait a moment, there are no Journal elements yet." +msgstr "Wait a moment, there are no Journal elements yet." + +#: markup_doc/wagtail_hooks.py +msgid "Synchronizing journals from the API, please wait a moment..." +msgstr "Synchronizing journals from the API, please wait a moment..." \ No newline at end of file diff --git a/locale/es/LC_MESSAGES/django.po b/locale/es/LC_MESSAGES/django.po new file mode 100755 index 0000000..69ee7fe --- /dev/null +++ b/locale/es/LC_MESSAGES/django.po @@ -0,0 +1,1372 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-10-24 13:56+0000\n" +"PO-Revision-Date: 2025-10-27 HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Spanish \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? " +"1 : 2;\n" + +#: core/choices.py:192 +msgid "Editor-Chefe" +msgstr "Editor en Jefe" + +#: core/choices.py:193 +msgid "Editor(es) Executivo" +msgstr "Editor(es) Ejecutivo" + +#: core/choices.py:194 +msgid "Editor(es) Associados ou de Seção" +msgstr "Editor(es) Asociados o de Sección" + +#: core/choices.py:195 +msgid "Equipe Técnica" +msgstr "Equipo Técnico" + +#: core/choices.py:199 +msgid "January" +msgstr "Enero" + +#: core/choices.py:200 +msgid "February" +msgstr "Febrero" + +#: core/choices.py:201 +msgid "March" +msgstr "Marzo" + +#: core/choices.py:202 +msgid "April" +msgstr "Abril" + +#: core/choices.py:203 +msgid "May" +msgstr "Mayo" + +#: core/choices.py:204 +msgid "June" +msgstr "Junio" + +#: core/choices.py:205 +msgid "July" +msgstr "Julio" + +#: core/choices.py:206 +msgid "August" +msgstr "Agosto" + +#: core/choices.py:207 +msgid "September" +msgstr "Septiembre" + +#: core/choices.py:208 +msgid "October" +msgstr "Octubre" + +#: core/choices.py:209 +msgid "November" +msgstr "Noviembre" + +#: core/choices.py:210 +msgid "December" +msgstr "Diciembre" + +#: core/choices.py:216 +msgid "by" +msgstr "por" + +#: core/choices.py:217 +msgid "by-sa" +msgstr "by-sa" + +#: core/choices.py:218 +msgid "by-nc" +msgstr "by-nc" + +#: core/choices.py:219 +msgid "by-nc-sa" +msgstr "by-nc-sa" + +#: core/choices.py:220 +msgid "by-nd" +msgstr "by-nd" + +#: core/choices.py:221 +msgid "by-nc-nd" +msgstr "by-nc-nd" + +#: core/choices.py:225 +msgid "Male" +msgstr "Masculino" + +#: core/choices.py:226 +msgid "Female" +msgstr "Femenino" + +#: core/home/templates/home/welcome_page.html:6 +msgid "Visit the Wagtail website" +msgstr "Visita el sitio web de Wagtail" + +#: core/home/templates/home/welcome_page.html:15 +msgid "View the release notes" +msgstr "Ver las notas de lanzamiento" + +#: core/home/templates/home/welcome_page.html:27 +msgid "Welcome to your new Wagtail site!" +msgstr "¡Bienvenido a tu nuevo sitio Wagtail!" + +#: core/home/templates/home/welcome_page.html:35 +msgid "Wagtail Documentation" +msgstr "Documentación de Wagtail" + +#: core/home/templates/home/welcome_page.html:36 +msgid "Topics, references, & how-tos" +msgstr "Temas, referencias y guías prácticas" + +#: core/home/templates/home/welcome_page.html:42 +msgid "Tutorial" +msgstr "Tutorial" + +#: core/home/templates/home/welcome_page.html:43 +msgid "Build your first Wagtail site" +msgstr "Construye tu primer sitio Wagtail" + +#: core/home/templates/home/welcome_page.html:49 +msgid "Admin Interface" +msgstr "Interfaz de Administración" + +#: core/home/templates/home/welcome_page.html:50 +msgid "Create your superuser first!" +msgstr "¡Crea tu superusuario primero!" + +#: core/models.py:28 tracker/models.py:76 +msgid "Creation date" +msgstr "Fecha de creación" + +#: core/models.py:31 +msgid "Last update date" +msgstr "Fecha de última actualización" + +#: core/models.py:36 +msgid "Creator" +msgstr "Creador" + +#: core/models.py:46 +msgid "Updater" +msgstr "Actualizador" + +#: core/models.py:66 +msgid "Code" +msgstr "Código" + +#: core/models.py:68 +msgid "Sex" +msgstr "Sexo" + +#: core/models.py:133 +msgid "Language Name" +msgstr "Nombre del Idioma" + +#: core/models.py:134 +msgid "Language code 2" +msgstr "Código de idioma 2" + +#: core/models.py:142 core/models.py:190 core/models.py:207 core/models.py:251 +#: core/models.py:523 markup_doc/models.py:357 xml_manager/models.py:58 +#: xml_manager/models.py:90 +msgid "Language" +msgstr "Idioma" + +#: core/models.py:143 +msgid "Languages" +msgstr "Idiomas" + +#: core/models.py:186 markup_doc/models.py:129 +msgid "Text" +msgstr "Texto" + +#: core/models.py:202 core/models.py:247 +msgid "Rich Text" +msgstr "Texto Enriquecido" + +#: core/models.py:203 +msgid "Plain Text" +msgstr "Texto Plano" + +#: core/models.py:268 +msgid "Year" +msgstr "Año" + +#: core/models.py:269 +msgid "Month" +msgstr "Mes" + +#: core/models.py:270 django_celery_beat/choices.py:18 +msgid "Day" +msgstr "Día" + +#: core/models.py:301 core/models.py:382 +msgid "License" +msgstr "Licencia" + +#: core/models.py:302 core/models.py:383 +msgid "Licenses" +msgstr "Licencias" + +#: core/models.py:515 +msgid "File" +msgstr "Archivo" + +#: core_settings/models.py:18 core_settings/models.py:19 +msgid "Site configuration" +msgstr "Configuración del sitio" + +#: core_settings/models.py:66 +msgid "Site settings" +msgstr "Configuración del sitio" + +#: core_settings/models.py:67 +msgid "Admin settings" +msgstr "Configuración de administración" + +#: django_celery_beat/admin.py:69 django_celery_beat/forms.py:55 +msgid "Task (registered)" +msgstr "Tarea (registrada)" + +#: django_celery_beat/admin.py:73 django_celery_beat/forms.py:59 +msgid "Task (custom)" +msgstr "Tarea (personalizada)" + +#: django_celery_beat/admin.py:90 django_celery_beat/forms.py:75 +msgid "Need name of task" +msgstr "Se necesita el nombre de la tarea" + +#: django_celery_beat/admin.py:96 django_celery_beat/forms.py:81 +#: django_celery_beat/models.py:648 +msgid "Only one can be set, in expires and expire_seconds" +msgstr "Solo uno puede establecerse, entre expires y expire_seconds" + +#: django_celery_beat/admin.py:106 django_celery_beat/forms.py:91 +#, python-format +msgid "Unable to parse JSON: %s" +msgstr "No se puede analizar JSON: %s" + +#: django_celery_beat/admin.py:207 django_celery_beat/wagtail_hooks.py:70 +#, python-brace-format +msgid "{0} task{1} {2} successfully {3}" +msgstr "{0} tarea{1} {2} exitosamente {3}" + +#: django_celery_beat/admin.py:210 django_celery_beat/admin.py:283 +#: django_celery_beat/wagtail_hooks.py:73 +#: django_celery_beat/wagtail_hooks.py:154 +msgid "was,were" +msgstr "fue,fueron" + +#: django_celery_beat/admin.py:220 django_celery_beat/wagtail_hooks.py:83 +msgid "Enable selected tasks" +msgstr "Habilitar tareas seleccionadas" + +#: django_celery_beat/admin.py:227 django_celery_beat/wagtail_hooks.py:90 +msgid "Disable selected tasks" +msgstr "Deshabilitar tareas seleccionadas" + +#: django_celery_beat/admin.py:242 django_celery_beat/wagtail_hooks.py:105 +msgid "Toggle activity of selected tasks" +msgstr "Alternar actividad de las tareas seleccionadas" + +#: django_celery_beat/admin.py:266 django_celery_beat/wagtail_hooks.py:130 +#, python-brace-format +msgid "task \"{0}\" not found" +msgstr "tarea \"{0}\" no encontrada" + +#: django_celery_beat/admin.py:280 django_celery_beat/wagtail_hooks.py:151 +#, python-brace-format +msgid "{0} task{1} {2} successfully run" +msgstr "{0} tarea{1} {2} ejecutada{1} exitosamente" + +#: django_celery_beat/admin.py:287 django_celery_beat/wagtail_hooks.py:158 +msgid "Run selected tasks" +msgstr "Ejecutar tareas seleccionadas" + +#: django_celery_beat/apps.py:13 +msgid "Periodic Tasks" +msgstr "Tareas Periódicas" + +#: django_celery_beat/button_helper.py:15 +msgid "Run" +msgstr "Ejecutar" + +#: django_celery_beat/choices.py:10 +msgid "Days" +msgstr "Días" + +#: django_celery_beat/choices.py:11 +msgid "Hours" +msgstr "Horas" + +#: django_celery_beat/choices.py:12 +msgid "Minutes" +msgstr "Minutos" + +#: django_celery_beat/choices.py:13 +msgid "Seconds" +msgstr "Segundos" + +#: django_celery_beat/choices.py:14 +msgid "Microseconds" +msgstr "Microsegundos" + +#: django_celery_beat/choices.py:19 +msgid "Hour" +msgstr "Hora" + +#: django_celery_beat/choices.py:20 +msgid "Minute" +msgstr "Minuto" + +#: django_celery_beat/choices.py:21 +msgid "Second" +msgstr "Segundo" + +#: django_celery_beat/choices.py:22 +msgid "Microsecond" +msgstr "Microsegundo" + +#: django_celery_beat/choices.py:26 +msgid "Astronomical dawn" +msgstr "Amanecer astronómico" + +#: django_celery_beat/choices.py:27 +msgid "Civil dawn" +msgstr "Amanecer civil" + +#: django_celery_beat/choices.py:28 +msgid "Nautical dawn" +msgstr "Amanecer náutico" + +#: django_celery_beat/choices.py:29 +msgid "Astronomical dusk" +msgstr "Anochecer astronómico" + +#: django_celery_beat/choices.py:30 +msgid "Civil dusk" +msgstr "Anochecer civil" + +#: django_celery_beat/choices.py:31 +msgid "Nautical dusk" +msgstr "Anochecer náutico" + +#: django_celery_beat/choices.py:32 +msgid "Dawn" +msgstr "Amanecer" + +#: django_celery_beat/choices.py:33 +msgid "Sunrise" +msgstr "Salida del sol" + +#: django_celery_beat/choices.py:34 +msgid "Solar noon" +msgstr "Mediodía solar" + +#: django_celery_beat/choices.py:35 +msgid "Sunset" +msgstr "Puesta del sol" + +#: django_celery_beat/choices.py:36 +msgid "Dusk" +msgstr "Anochecer" + +#: django_celery_beat/forms.py:139 +msgid "Crontab expression is invalid. Please check the input fields." +msgstr "La expresión crontab es inválida. Por favor verifica los campos de entrada." + +#: django_celery_beat/models.py:24 +msgid "interval" +msgstr "intervalo" + +#: django_celery_beat/models.py:25 +msgid "intervals" +msgstr "intervalos" + +#: django_celery_beat/models.py:52 +msgid "Minute (integer from 0-59)" +msgstr "Minuto (entero de 0-59)" + +#: django_celery_beat/models.py:54 +msgid "Hour (integer from 0-23)" +msgstr "Hora (entero de 0-23)" + +#: django_celery_beat/models.py:56 +msgid "Day of the week (0-6, 0 is Sunday, or 'mon', 'tue', etc.)" +msgstr "Día de la semana (0-6, 0 es Domingo, o 'mon', 'tue', etc.)" + +#: django_celery_beat/models.py:59 +msgid "Day of the month (1-31)" +msgstr "Día del mes (1-31)" + +#: django_celery_beat/models.py:61 +msgid "Month of the year (1-12)" +msgstr "Mes del año (1-12)" + +#: django_celery_beat/models.py:63 +msgid "Timezone" +msgstr "Zona horaria" + +#: django_celery_beat/models.py:92 +msgid "The human-readable timezone name that this schedule will follow (e.g. 'Europe/Berlin')" +msgstr "El nombre legible de la zona horaria que seguirá este horario (ej. 'Europe/Berlin')" + +#: django_celery_beat/models.py:116 +msgid "crontab" +msgstr "crontab" + +#: django_celery_beat/models.py:117 +msgid "crontabs" +msgstr "crontabs" + +#: django_celery_beat/models.py:137 +msgid "Name" +msgstr "Nombre" + +#: django_celery_beat/models.py:138 +msgid "Event" +msgstr "Evento" + +#: django_celery_beat/models.py:139 +msgid "Latitude" +msgstr "Latitud" + +#: django_celery_beat/models.py:141 +msgid "Longitude" +msgstr "Longitud" + +#: django_celery_beat/models.py:171 +msgid "solar event" +msgstr "evento solar" + +#: django_celery_beat/models.py:172 +msgid "solar events" +msgstr "eventos solares" + +#: django_celery_beat/models.py:184 +msgid "Clocked Time" +msgstr "Hora Programada" + +#: django_celery_beat/models.py:191 +msgid "clocked schedule" +msgstr "horario programado" + +#: django_celery_beat/models.py:192 +msgid "clocked schedules" +msgstr "horarios programados" + +#: django_celery_beat/models.py:290 +msgid "seconds" +msgstr "segundos" + +#: django_celery_beat/models.py:295 +msgid "positional arguments" +msgstr "argumentos posicionales" + +#: django_celery_beat/models.py:297 +msgid "JSON encoded positional arguments (Example: [\"arg1\", \"arg2\"])" +msgstr "Argumentos posicionales codificados en JSON (Ejemplo: [\"arg1\", \"arg2\"])" + +#: django_celery_beat/models.py:302 +msgid "keyword arguments" +msgstr "argumentos de palabras clave" + +#: django_celery_beat/models.py:304 +msgid "JSON encoded keyword arguments (Example: {\"argument\": \"value\"})" +msgstr "Argumentos de palabras clave codificados en JSON (Ejemplo: {\"argument\": \"value\"})" + +#: django_celery_beat/models.py:309 +msgid "queue" +msgstr "cola" + +#: django_celery_beat/models.py:313 +msgid "exchange" +msgstr "intercambio" + +#: django_celery_beat/models.py:317 +msgid "routing key" +msgstr "clave de enrutamiento" + +#: django_celery_beat/models.py:321 +msgid "headers" +msgstr "encabezados" + +#: django_celery_beat/models.py:323 +msgid "JSON encoded message headers for the AMQP message." +msgstr "Encabezados de mensaje codificados en JSON para el mensaje AMQP." + +#: django_celery_beat/models.py:327 +msgid "priority" +msgstr "prioridad" + +#: django_celery_beat/models.py:329 +msgid "Priority Number between 0 and 255. Supported by: RabbitMQ." +msgstr "Número de prioridad entre 0 y 255. Compatible con: RabbitMQ." + +#: django_celery_beat/models.py:336 +msgid "expires" +msgstr "expira" + +#: django_celery_beat/models.py:338 +msgid "Datetime after which the schedule will no longer trigger the task to run" +msgstr "Fecha y hora después de la cual el horario ya no activará la ejecución de la tarea" + +#: django_celery_beat/models.py:343 +msgid "expire seconds" +msgstr "segundos de expiración" + +#: django_celery_beat/models.py:345 +msgid "Timedelta with seconds which the schedule will no longer trigger the task to run" +msgstr "Timedelta en segundos después del cual el horario ya no activará la ejecución de la tarea" + +#: django_celery_beat/models.py:350 +msgid "one-off task" +msgstr "tarea única" + +#: django_celery_beat/models.py:352 +msgid "If True, the schedule will only run the task a single time" +msgstr "Si es True, el horario solo ejecutará la tarea una vez" + +#: django_celery_beat/models.py:356 +msgid "start datetime" +msgstr "fecha y hora de inicio" + +#: django_celery_beat/models.py:358 +msgid "Datetime when the schedule should begin triggering the task to run" +msgstr "Fecha y hora en que el horario debe comenzar a activar la ejecución de la tarea" + +#: django_celery_beat/models.py:363 +msgid "enabled" +msgstr "habilitado" + +#: django_celery_beat/models.py:365 +msgid "Set to False to disable the schedule" +msgstr "Establecer en False para deshabilitar el horario" + +#: django_celery_beat/models.py:369 +msgid "last run at" +msgstr "última ejecución en" + +#: django_celery_beat/models.py:371 +msgid "Datetime that the schedule last triggered the task to run. Reset to None if enabled is set to False." +msgstr "Fecha y hora en que el horario activó por última vez la ejecución de la tarea. Se restablece a None si enabled se establece en False." + +#: django_celery_beat/models.py:376 +msgid "total run count" +msgstr "conteo total de ejecuciones" + +#: django_celery_beat/models.py:378 +msgid "Running count of how many times the schedule has triggered the task" +msgstr "Conteo de cuántas veces el horario ha activado la tarea" + +#: django_celery_beat/models.py:382 +msgid "datetime changed" +msgstr "fecha y hora modificada" + +#: django_celery_beat/models.py:384 +msgid "Datetime that this PeriodicTask was last modified" +msgstr "Fecha y hora en que esta PeriodicTask fue modificada por última vez" + +#: django_celery_beat/models.py:388 +msgid "description" +msgstr "descripción" + +#: django_celery_beat/models.py:390 +msgid "Detailed description about the details of this Periodic Task" +msgstr "Descripción detallada sobre los detalles de esta Tarea Periódica" + +#: django_celery_beat/models.py:638 +msgid "Only one schedule can be selected: Interval, Crontab, Solar, or Clocked" +msgstr "Solo se puede seleccionar un horario: Intervalo, Crontab, Solar o Programado" + +#: django_celery_beat/models.py:642 +msgid "One of interval, crontab, solar, or clocked must be set." +msgstr "Debe establecerse uno de: intervalo, crontab, solar o programado." + +#: django_celery_beat/models.py:668 +msgid "periodic task" +msgstr "tarea periódica" + +#: django_celery_beat/models.py:669 +msgid "periodic tasks" +msgstr "tareas periódicas" + +#: django_celery_beat/wagtail_hooks.py:217 +msgid "enabled,disabled" +msgstr "habilitada,deshabilitada" + +#: django_celery_beat/wagtail_hooks.py:224 +msgid "Periodic tasks" +msgstr "Tareas periódicas" + +#: journal/models.py:27 +msgid "Site ID" +msgstr "ID del Sitio" + +#: journal/models.py:29 +msgid "Short title" +msgstr "Título corto" + +#: journal/models.py:31 +msgid "Journal ISSN" +msgstr "ISSN de la Revista" + +#: journal/models.py:33 +msgid "Online Journal ISSN" +msgstr "ISSN de la Revista en Línea" + +#: journal/models.py:36 +msgid "ISSN SciELO" +msgstr "ISSN SciELO" + +#: journal/models.py:38 +msgid "ISSN SciELO Legacy" +msgstr "ISSN SciELO Legacy" + +#: journal/models.py:41 +msgid "Subject descriptors" +msgstr "Descriptores de tema" + +#: journal/models.py:46 +msgid "Purpose" +msgstr "Propósito" + +#: journal/models.py:49 +msgid "Sponsorship" +msgstr "Patrocinio" + +#: journal/models.py:52 +msgid "Mission" +msgstr "Misión" + +#: journal/models.py:55 +msgid "Index at" +msgstr "Indexado en" + +#: journal/models.py:58 +msgid "Availability" +msgstr "Disponibilidad" + +#: journal/models.py:61 +msgid "Summary form" +msgstr "Forma de resumen" + +#: journal/models.py:64 +msgid "Standard" +msgstr "Estándar" + +#: journal/models.py:67 +msgid "Alphabet title" +msgstr "Título alfabético" + +#: journal/models.py:69 +msgid "Print title" +msgstr "Título impreso" + +#: journal/models.py:72 +msgid "Short title (slug)" +msgstr "Título corto (slug)" + +#: journal/models.py:76 +#: markup_doc/models.py:40 +msgid "Title" +msgstr "Título" + +#: journal/models.py:81 +msgid "Subtitle" +msgstr "Subtítulo" + +#: journal/models.py:86 +msgid "Next title" +msgstr "Título siguiente" + +#: journal/models.py:90 +msgid "Previous title" +msgstr "Título anterior" + +#: journal/models.py:94 +msgid "Control number" +msgstr "Número de control" + +#: journal/models.py:97 +msgid "Publisher name" +msgstr "Nombre del editor" + +#: journal/models.py:99 +msgid "Publisher country" +msgstr "País del editor" + +#: journal/models.py:102 +msgid "Publisher state" +msgstr "Estado del editor" + +#: journal/models.py:105 +msgid "Publisher city" +msgstr "Ciudad del editor" + +#: journal/models.py:108 +msgid "Publisher address" +msgstr "Dirección del editor" + +#: journal/models.py:111 +msgid "Publication level" +msgstr "Nivel de publicación" + +#: journal/models.py:115 +msgid "Email" +msgstr "Correo electrónico" + +#: journal/models.py:119 +msgid "URL online submission" +msgstr "URL de envío en línea" + +#: journal/models.py:122 +msgid "URL home page" +msgstr "URL de página principal" + +#: journal/models.py:146 +msgid "Journal" +msgstr "Revista" + +#: journal/models.py:147 +msgid "Journals" +msgstr "Revistas" + +#: journal/models.py:162 +msgid "Order" +msgstr "Orden" + +#: journal/models.py:174 +msgid "Editor" +msgstr "Editor" + +#: journal/models.py:175 +msgid "Editors" +msgstr "Editores" + +#: journal/wagtail_hooks.py:57 +msgid "Journal and Editor" +msgstr "Revista y Editor" + +#: location/models.py:11 +msgid "Country Name" +msgstr "Nombre del País" + +#: location/models.py:12 +msgid "ACR3" +msgstr "ACR3" + +#: location/models.py:13 +msgid "ACR2" +msgstr "ACR2" + +#: location/models.py:23 +msgid "Country" +msgstr "País" + +#: location/models.py:24 +msgid "Countries" +msgstr "Países" + +#: location/models.py:36 +msgid "State name" +msgstr "Nombre del estado" + +#: location/models.py:37 +msgid "ACR 2 (state)" +msgstr "ACR 2 (estado)" + +#: location/models.py:48 +msgid "State" +msgstr "Estado" + +#: location/models.py:49 +msgid "States" +msgstr "Estados" + +#: location/models.py:61 +msgid "City name" +msgstr "Nombre de la ciudad" + +#: location/models.py:75 +msgid "City" +msgstr "Ciudad" + +#: location/models.py:76 +msgid "Cities" +msgstr "Ciudades" + +#: location/wagtail_hooks.py:55 +msgid "Location" +msgstr "Ubicación" + +#: markup_doc/choices.py:11 +msgid "Document without content" +msgstr "Documento sin contenido" + +#: markup_doc/choices.py:12 +msgid "Structured document without references" +msgstr "Documento estructurado sin referencias" + +#: markup_doc/choices.py:13 +msgid "Structured document" +msgstr "Documento estructurado" + +#: markup_doc/choices.py:14 +msgid "Structured document and references" +msgstr "Documento estructurado y referencias" + +#: markup_doc/models.py:33 +msgid "DOI" +msgstr "DOI" + +#: markup_doc/models.py:56 +msgid "Rich Title" +msgstr "Título Enriquecido" + +#: markup_doc/models.py:58 +msgid "Clean Title" +msgstr "Título Limpio" + +#: markup_doc/models.py:97 +msgid "Document" +msgstr "Documento" + +#: markup_doc/models.py:98 +msgid "Documents" +msgstr "Documentos" + +#: markup_doc/models.py:130 +msgid "Type" +msgstr "Tipo" + +#: markup_doc/models.py:137 +msgid "Paragraph" +msgstr "Párrafo" + +#: markup_doc/models.py:138 +msgid "Paragraphs" +msgstr "Párrafos" + +#: markup_doc/models.py:218 +msgid "Section title" +msgstr "Título de sección" + +#: markup_doc/models.py:222 +msgid "Section code" +msgstr "Código de sección" + +#: markup_doc/models.py:245 +msgid "Section" +msgstr "Sección" + +#: markup_doc/models.py:246 +msgid "Sections" +msgstr "Secciones" + +#: markup_doc/models.py:279 +msgid "Figure" +msgstr "Figura" + +#: markup_doc/models.py:280 +msgid "Figures" +msgstr "Figuras" + +#: markup_doc/models.py:306 +msgid "Table" +msgstr "Tabla" + +#: markup_doc/models.py:307 +msgid "Tables" +msgstr "Tablas" + +#: markup_doc/models.py:322 +msgid "Supplementary Material" +msgstr "Material Suplementario" + +#: markup_doc/models.py:323 +msgid "Supplementary Materials" +msgstr "Materiales Suplementarios" + +#: markup_doc/models.py:336 +msgid "Document ID" +msgstr "ID de Documento" + +#: markup_doc/models.py:340 +msgid "Document ID type" +msgstr "Tipo de ID de documento" + +#: markup_doc/models.py:349 +msgid "DOCX ID" +msgstr "ID de DOCX" + +#: markup_doc/models.py:350 +msgid "DOCX IDs" +msgstr "IDs de DOCX" + +#: markup_doc/models.py:360 +msgid "DOCX Subtitle" +msgstr "Subtítulo de DOCX" + +#: markup_doc/models.py:377 +msgid "DOCX Body" +msgstr "Cuerpo de DOCX" + +#: markup_doc/models.py:378 +msgid "DOCX Bodies" +msgstr "Cuerpos de DOCX" + +#: markup_doc/models.py:386 +msgid "DOCX Section" +msgstr "Sección de DOCX" + +#: markup_doc/models.py:387 +msgid "DOCX Sections" +msgstr "Secciones de DOCX" + +#: markup_doc/models.py:400 +msgid "Legend" +msgstr "Leyenda" + +#: markup_doc/models.py:405 +msgid "Caption" +msgstr "Pie de imagen" + +#: markup_doc/models.py:409 +msgid "Source" +msgstr "Fuente" + +#: markup_doc/models.py:410 +msgid "Target" +msgstr "Destino" + +#: markup_doc/models.py:413 +msgid "Content type" +msgstr "Tipo de contenido" + +#: markup_doc/models.py:414 +msgid "MIME Type" +msgstr "Tipo MIME" + +#: markup_doc/models.py:428 +msgid "DOCX Figure" +msgstr "Figura de DOCX" + +#: markup_doc/models.py:429 +msgid "DOCX Figures" +msgstr "Figuras de DOCX" + +#: markup_doc/models.py:442 +msgid "Columns" +msgstr "Columnas" + +#: markup_doc/models.py:443 +msgid "Rows" +msgstr "Filas" + +#: markup_doc/models.py:451 +msgid "DOCX Table" +msgstr "Tabla de DOCX" + +#: markup_doc/models.py:452 +msgid "DOCX Tables" +msgstr "Tablas de DOCX" + +#: markup_doc/wagtail_hooks.py:91 +msgid "MarkupDoc" +msgstr "MarkupDoc" + +#: model_ai/models.py:17 +msgid "Model Provider" +msgstr "Proveedor del Modelo" + +#: model_ai/models.py:22 +msgid "Model Name" +msgstr "Nombre del Modelo" + +#: model_ai/models.py:27 +msgid "Model Type" +msgstr "Tipo de Modelo" + +#: model_ai/models.py:32 +msgid "Context length" +msgstr "Longitud del contexto" + +#: model_ai/models.py:34 +msgid "Hugging Face model name" +msgstr "Nombre del modelo de Hugging Face" + +#: model_ai/models.py:35 +msgid "Model file" +msgstr "Archivo del modelo" + +#: model_ai/models.py:36 +msgid "Hugging Face token" +msgstr "Token de Hugging Face" + +#: model_ai/models.py:38 +msgid "Local model status" +msgstr "Estado del modelo local" + +#: model_ai/models.py:44 +msgid "URL Markapi" +msgstr "URL de Markapi" + +#: model_ai/models.py:49 +msgid "API KEY Gemini" +msgstr "Clave API de Gemini" + +#: model_ai/models.py:67 model_ai/models.py:71 +msgid "Only one instance of LlamaModel is allowed." +msgstr "Solo se permite una instancia de LlamaModel." + +#: model_ai/wagtail_hooks.py:28 model_ai/wagtail_hooks.py:63 +msgid "Model name is required." +msgstr "El nombre del modelo es obligatorio." + +#: model_ai/wagtail_hooks.py:32 model_ai/wagtail_hooks.py:67 +msgid "Model file is required." +msgstr "El archivo del modelo es obligatorio." + +#: model_ai/wagtail_hooks.py:36 model_ai/wagtail_hooks.py:71 +msgid "Hugging Face token is required." +msgstr "El token de Hugging Face es obligatorio." + +#: model_ai/wagtail_hooks.py:42 +msgid "Model created and download started." +msgstr "Modelo creado y descarga iniciada." + +#: model_ai/wagtail_hooks.py:46 model_ai/wagtail_hooks.py:82 +msgid "API AI URL is required." +msgstr "La URL de API AI es obligatoria." + +#: model_ai/wagtail_hooks.py:50 +msgid "Model created, use API AI." +msgstr "Modelo creado, usar API AI." + +#: model_ai/wagtail_hooks.py:77 +msgid "Model updated and download started." +msgstr "Modelo actualizado y descarga iniciada." + +#: model_ai/wagtail_hooks.py:79 +msgid "Model updated and already downloaded." +msgstr "Modelo actualizado y ya descargado." + +#: model_ai/wagtail_hooks.py:86 +msgid "Model updated, use API AI." +msgstr "Modelo actualizado, usar API AI." + +#: model_ai/wagtail_hooks.py:95 +msgid "AI LLM Model" +msgstr "Modelo de LLM de IA" + +#: reference/models.py:15 +msgid "No reference" +msgstr "Sin referencia" + +#: reference/models.py:16 +msgid "Creating reference" +msgstr "Creando referencia" + +#: reference/models.py:17 +msgid "Reference ready" +msgstr "Referencia lista" + +#: reference/models.py:22 +msgid "Mixed Citation" +msgstr "Cita mixta" + +#: reference/models.py:25 +msgid "Reference status" +msgstr "Estado de referencia" + +#: reference/models.py:33 +msgid "Cited Elements" +msgstr "Elementos Citados" + +#: reference/models.py:46 +msgid "Marked" +msgstr "Marcado" + +#: reference/models.py:47 +msgid "Marked XML" +msgstr "XML Marcado" + +#: reference/models.py:56 +msgid "Rating from 1 to 10" +msgstr "Calificación del 1 al 10" + +#: reference/wagtail_hooks.py:41 +msgid "Reference" +msgstr "Referencia" + +#: tracker/choices.py:9 +msgid "error" +msgstr "error" + +#: tracker/choices.py:10 +msgid "warning" +msgstr "advertencia" + +#: tracker/choices.py:11 +msgid "info" +msgstr "información" + +#: tracker/choices.py:12 +msgid "exception" +msgstr "excepción" + +#: tracker/choices.py:24 +msgid "To reprocess" +msgstr "Para reprocesar" + +#: tracker/choices.py:25 +msgid "To do" +msgstr "Por hacer" + +#: tracker/choices.py:26 +msgid "Done" +msgstr "Completado" + +#: tracker/choices.py:27 +msgid "Doing" +msgstr "Haciendo" + +#: tracker/choices.py:28 +msgid "Pending" +msgstr "Pendiente" + +#: tracker/choices.py:29 +msgid "ignored" +msgstr "ignorado" + +#: tracker/choices.py:41 +msgid "XML Parsing Error" +msgstr "Error de Análisis XML" + +#: tracker/choices.py:42 +msgid "XML Validation Error" +msgstr "Error de Validación XML" + +#: tracker/choices.py:43 +msgid "XML Conversion to DOCX Error" +msgstr "Error de Conversión XML a DOCX" + +#: tracker/choices.py:44 +msgid "XML Conversion to HTML Error" +msgstr "Error de Conversión XML a HTML" + +#: tracker/choices.py:45 +msgid "XML Conversion to PDF Error" +msgstr "Error de Conversión XML a PDF" + +#: tracker/choices.py:46 +msgid "XML Conversion to TEX Error" +msgstr "Error de Conversión XML a TEX" + +#: tracker/choices.py:47 +msgid "Unknown Error" +msgstr "Error Desconocido" + +#: tracker/models.py:28 +msgid "Error Type" +msgstr "Tipo de Error" + +#: tracker/models.py:35 +msgid "Data" +msgstr "Datos" + +#: tracker/models.py:39 +msgid "Message" +msgstr "Mensaje" + +#: tracker/models.py:44 +msgid "Handled" +msgstr "Manejado" + +#: tracker/models.py:70 +msgid "XML Document Event" +msgstr "Evento de Documento XML" + +#: tracker/models.py:71 tracker/wagtail_hooks.py:16 +msgid "XML Document Events" +msgstr "Eventos de Documento XML" + +#: tracker/models.py:77 +msgid "Exception Type" +msgstr "Tipo de Excepción" + +#: tracker/models.py:78 +msgid "Exception Msg" +msgstr "Mensaje de Excepción" + +#: tracker/models.py:82 +msgid "Item" +msgstr "Elemento" + +#: tracker/models.py:88 xml_manager/wagtail_hooks.py:69 +msgid "Action" +msgstr "Acción" + +#: tracker/models.py:101 +msgid "General Event" +msgstr "Evento General" + +#: tracker/models.py:102 tracker/wagtail_hooks.py:47 +msgid "General Events" +msgstr "Eventos Generales" + +#: tracker/wagtail_hooks.py:81 +msgid "Unexpected Events" +msgstr "Eventos Inesperados" + +#: users/admin.py:13 +msgid "Personal info" +msgstr "Información personal" + +#: users/admin.py:15 +msgid "Permissions" +msgstr "Permisos" + +#: users/admin.py:26 +msgid "Important dates" +msgstr "Fechas importantes" + +#: users/forms.py:11 +msgid "This username has already been taken." +msgstr "Este nombre de usuario ya ha sido tomado." + +#: users/models.py:15 +msgid "Name of User" +msgstr "Nombre del Usuario" + +#: xml_manager/models.py:9 +msgid "XML File" +msgstr "Archivo XML" + +#: xml_manager/models.py:10 +msgid "Upload an XML file for processing." +msgstr "Subir un archivo XML para procesamiento." + +#: xml_manager/models.py:16 +msgid "Validation File" +msgstr "Archivo de Validación" + +#: xml_manager/models.py:22 +msgid "Exceptions File" +msgstr "Archivo de Excepciones" + +#: xml_manager/models.py:26 xml_manager/models.py:61 xml_manager/models.py:93 +msgid "Uploaded At" +msgstr "Subido En" + +#: xml_manager/models.py:27 +msgid "The date and time when the file was uploaded." +msgstr "La fecha y hora cuando el archivo fue subido." + +#: xml_manager/models.py:38 xml_manager/models.py:43 xml_manager/models.py:82 +#: xml_manager/wagtail_hooks.py:56 xml_manager/wagtail_hooks.py:60 +msgid "XML Document" +msgstr "Documento XML" + +#: xml_manager/models.py:39 xml_manager/wagtail_hooks.py:57 +msgid "XML Documents" +msgstr "Documentos XML" + +#: xml_manager/models.py:46 +msgid "PDF File" +msgstr "Archivo PDF" + +#: xml_manager/models.py:50 +msgid "DOCX File" +msgstr "Archivo DOCX" + +#: xml_manager/models.py:53 +msgid "Intermediate DOCX file generated during PDF creation" +msgstr "Archivo DOCX intermedio generado durante la creación del PDF" + +#: xml_manager/models.py:59 xml_manager/models.py:91 +msgid "Language code or name" +msgstr "Código o nombre del idioma" + +#: xml_manager/models.py:67 xml_manager/wagtail_hooks.py:77 +#: xml_manager/wagtail_hooks.py:81 +msgid "XML Document PDF" +msgstr "PDF de Documento XML" + +#: xml_manager/models.py:68 xml_manager/wagtail_hooks.py:78 +msgid "XML Document PDFs" +msgstr "PDFs de Documento XML" + +#: xml_manager/models.py:85 +msgid "HTML File" +msgstr "Archivo HTML" + +#: xml_manager/models.py:99 xml_manager/wagtail_hooks.py:98 +#: xml_manager/wagtail_hooks.py:102 +msgid "XML Document HTML" +msgstr "HTML de Documento XML" + +#: xml_manager/models.py:100 xml_manager/wagtail_hooks.py:99 +msgid "XML Document HTMLs" +msgstr "HTMLs de Documento XML" + +#: xml_manager/wagtail_hooks.py:66 +msgid "Validation file" +msgstr "Archivo de validación" + +#: xml_manager/wagtail_hooks.py:67 +msgid "Exceptions file" +msgstr "Archivo de excepciones" + +#: xml_manager/wagtail_hooks.py:118 +msgid "XML Processor" +msgstr "Procesador XML" + +#: menu items +msgid "DOCX Files" +msgstr "Archivos DOCX" + +#: menu items +msgid "XML Files" +msgstr "Archivos XML" + +#: menu items +msgid "Tarefas" +msgstr "Tareas" + +#: menu items +msgid "Carregar DOCX" +msgstr "Cargar DOCX" + +#: menu items +msgid "XML marcado" +msgstr "XML marcado" + +#: markup_doc/wagtail_hooks.py +msgid "You must first select a collection." +msgstr "Debes seleccionar primero una colección." + +#: markup_doc/wagtail_hooks.py +msgid "Wait a moment, there are no Journal elements yet." +msgstr "Espera un momento, aún no existen elementos en Journal." + +#: markup_doc/wagtail_hooks.py +msgid "Synchronizing journals from the API, please wait a moment..." +msgstr "Sincronizando revistas desde la API, espera unos momentos..." \ No newline at end of file diff --git a/locale/pt_BR/LC_MESSAGES/django.po b/locale/pt_BR/LC_MESSAGES/django.po new file mode 100755 index 0000000..8e4bf5a --- /dev/null +++ b/locale/pt_BR/LC_MESSAGES/django.po @@ -0,0 +1,1413 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-10-24 13:56+0000\n" +"PO-Revision-Date: 2025-10-27 HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Portuguese \n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: core/choices.py:192 +msgid "Editor-Chefe" +msgstr "Editor-Chefe" + +#: core/choices.py:193 +msgid "Editor(es) Executivo" +msgstr "Editor(es) Executivo" + +#: core/choices.py:194 +msgid "Editor(es) Associados ou de Seção" +msgstr "Editor(es) Associados ou de Seção" + +#: core/choices.py:195 +msgid "Equipe Técnica" +msgstr "Equipe Técnica" + +#: core/choices.py:199 +msgid "January" +msgstr "Janeiro" + +#: core/choices.py:200 +msgid "February" +msgstr "Fevereiro" + +#: core/choices.py:201 +msgid "March" +msgstr "Março" + +#: core/choices.py:202 +msgid "April" +msgstr "Abril" + +#: core/choices.py:203 +msgid "May" +msgstr "Maio" + +#: core/choices.py:204 +msgid "June" +msgstr "Junho" + +#: core/choices.py:205 +msgid "July" +msgstr "Julho" + +#: core/choices.py:206 +msgid "August" +msgstr "Agosto" + +#: core/choices.py:207 +msgid "September" +msgstr "Setembro" + +#: core/choices.py:208 +msgid "October" +msgstr "Outubro" + +#: core/choices.py:209 +msgid "November" +msgstr "Novembro" + +#: core/choices.py:210 +msgid "December" +msgstr "Dezembro" + +#: core/choices.py:216 +msgid "by" +msgstr "por" + +#: core/choices.py:217 +msgid "by-sa" +msgstr "by-sa" + +#: core/choices.py:218 +msgid "by-nc" +msgstr "by-nc" + +#: core/choices.py:219 +msgid "by-nc-sa" +msgstr "by-nc-sa" + +#: core/choices.py:220 +msgid "by-nd" +msgstr "by-nd" + +#: core/choices.py:221 +msgid "by-nc-nd" +msgstr "by-nc-nd" + +#: core/choices.py:225 +msgid "Male" +msgstr "Masculino" + +#: core/choices.py:226 +msgid "Female" +msgstr "Feminino" + +#: core/home/templates/home/welcome_page.html:6 +msgid "Visit the Wagtail website" +msgstr "Visite o site do Wagtail" + +#: core/home/templates/home/welcome_page.html:15 +msgid "View the release notes" +msgstr "Ver as notas de lançamento" + +#: core/home/templates/home/welcome_page.html:27 +msgid "Welcome to your new Wagtail site!" +msgstr "Bem-vindo ao seu novo site Wagtail!" + +#: core/home/templates/home/welcome_page.html:35 +msgid "Wagtail Documentation" +msgstr "Documentação do Wagtail" + +#: core/home/templates/home/welcome_page.html:36 +msgid "Topics, references, & how-tos" +msgstr "Tópicos, referências e guias práticos" + +#: core/home/templates/home/welcome_page.html:42 +msgid "Tutorial" +msgstr "Tutorial" + +#: core/home/templates/home/welcome_page.html:43 +msgid "Build your first Wagtail site" +msgstr "Construa seu primeiro site Wagtail" + +#: core/home/templates/home/welcome_page.html:49 +msgid "Admin Interface" +msgstr "Interface de Administração" + +#: core/home/templates/home/welcome_page.html:50 +msgid "Create your superuser first!" +msgstr "Crie seu superusuário primeiro!" + +#: core/models.py:28 tracker/models.py:76 +msgid "Creation date" +msgstr "Data de criação" + +#: core/models.py:31 +msgid "Last update date" +msgstr "Data da última atualização" + +#: core/models.py:36 +msgid "Creator" +msgstr "Criador" + +#: core/models.py:46 +msgid "Updater" +msgstr "Atualizador" + +#: core/models.py:66 +msgid "Code" +msgstr "Código" + +#: core/models.py:68 +msgid "Sex" +msgstr "Sexo" + +#: core/models.py:133 +msgid "Language Name" +msgstr "Nome do Idioma" + +#: core/models.py:134 +msgid "Language code 2" +msgstr "Código do idioma 2" + +#: core/models.py:142 core/models.py:190 core/models.py:207 core/models.py:251 +#: core/models.py:523 markup_doc/models.py:357 xml_manager/models.py:58 +#: xml_manager/models.py:90 +msgid "Language" +msgstr "Idioma" + +#: core/models.py:143 +msgid "Languages" +msgstr "Idiomas" + +#: core/models.py:186 markup_doc/models.py:129 +msgid "Text" +msgstr "Texto" + +#: core/models.py:202 core/models.py:247 +msgid "Rich Text" +msgstr "Texto Rico" + +#: core/models.py:203 +msgid "Plain Text" +msgstr "Texto Simples" + +#: core/models.py:268 +msgid "Year" +msgstr "Ano" + +#: core/models.py:269 +msgid "Month" +msgstr "Mês" + +#: core/models.py:270 django_celery_beat/choices.py:18 +msgid "Day" +msgstr "Dia" + +#: core/models.py:301 core/models.py:382 +msgid "License" +msgstr "Licença" + +#: core/models.py:302 core/models.py:383 +msgid "Licenses" +msgstr "Licenças" + +#: core/models.py:515 +msgid "File" +msgstr "Arquivo" + +#: core_settings/models.py:18 core_settings/models.py:19 +msgid "Site configuration" +msgstr "Configuração do site" + +#: core_settings/models.py:66 +msgid "Site settings" +msgstr "Configurações do site" + +#: core_settings/models.py:67 +msgid "Admin settings" +msgstr "Configurações de administração" + +#: django_celery_beat/admin.py:69 django_celery_beat/forms.py:55 +msgid "Task (registered)" +msgstr "Tarefa (registrada)" + +#: django_celery_beat/admin.py:73 django_celery_beat/forms.py:59 +msgid "Task (custom)" +msgstr "Tarefa (personalizada)" + +#: django_celery_beat/admin.py:90 django_celery_beat/forms.py:75 +msgid "Need name of task" +msgstr "É necessário o nome da tarefa" + +#: django_celery_beat/admin.py:96 django_celery_beat/forms.py:81 +#: django_celery_beat/models.py:648 +msgid "Only one can be set, in expires and expire_seconds" +msgstr "Somente um pode ser definido, entre expires e expire_seconds" + +#: django_celery_beat/admin.py:106 django_celery_beat/forms.py:91 +#, python-format +msgid "Unable to parse JSON: %s" +msgstr "Não foi possível analisar JSON: %s" + +#: django_celery_beat/admin.py:207 django_celery_beat/wagtail_hooks.py:70 +#, python-brace-format +msgid "{0} task{1} {2} successfully {3}" +msgstr "{0} tarefa{1} {2} com sucesso {3}" + +#: django_celery_beat/admin.py:210 django_celery_beat/admin.py:283 +#: django_celery_beat/wagtail_hooks.py:73 +#: django_celery_beat/wagtail_hooks.py:154 +msgid "was,were" +msgstr "foi,foram" + +#: django_celery_beat/admin.py:220 django_celery_beat/wagtail_hooks.py:83 +msgid "Enable selected tasks" +msgstr "Habilitar tarefas selecionadas" + +#: django_celery_beat/admin.py:227 django_celery_beat/wagtail_hooks.py:90 +msgid "Disable selected tasks" +msgstr "Desabilitar tarefas selecionadas" + +#: django_celery_beat/admin.py:242 django_celery_beat/wagtail_hooks.py:105 +msgid "Toggle activity of selected tasks" +msgstr "Alternar atividade das tarefas selecionadas" + +#: django_celery_beat/admin.py:266 django_celery_beat/wagtail_hooks.py:130 +#, python-brace-format +msgid "task \"{0}\" not found" +msgstr "tarefa \"{0}\" não encontrada" + +#: django_celery_beat/admin.py:280 django_celery_beat/wagtail_hooks.py:151 +#, python-brace-format +msgid "{0} task{1} {2} successfully run" +msgstr "{0} tarefa{1} {2} executada{1} com sucesso" + +#: django_celery_beat/admin.py:287 django_celery_beat/wagtail_hooks.py:158 +msgid "Run selected tasks" +msgstr "Executar tarefas selecionadas" + +#: django_celery_beat/apps.py:13 +msgid "Periodic Tasks" +msgstr "Tarefas Periódicas" + +#: django_celery_beat/button_helper.py:15 +msgid "Run" +msgstr "Executar" + +#: django_celery_beat/choices.py:10 +msgid "Days" +msgstr "Dias" + +#: django_celery_beat/choices.py:11 +msgid "Hours" +msgstr "Horas" + +#: django_celery_beat/choices.py:12 +msgid "Minutes" +msgstr "Minutos" + +#: django_celery_beat/choices.py:13 +msgid "Seconds" +msgstr "Segundos" + +#: django_celery_beat/choices.py:14 +msgid "Microseconds" +msgstr "Microssegundos" + +#: django_celery_beat/choices.py:19 +msgid "Hour" +msgstr "Hora" + +#: django_celery_beat/choices.py:20 +msgid "Minute" +msgstr "Minuto" + +#: django_celery_beat/choices.py:21 +msgid "Second" +msgstr "Segundo" + +#: django_celery_beat/choices.py:22 +msgid "Microsecond" +msgstr "Microssegundo" + +#: django_celery_beat/choices.py:26 +msgid "Astronomical dawn" +msgstr "Alvorecer astronômico" + +#: django_celery_beat/choices.py:27 +msgid "Civil dawn" +msgstr "Alvorecer civil" + +#: django_celery_beat/choices.py:28 +msgid "Nautical dawn" +msgstr "Alvorecer náutico" + +#: django_celery_beat/choices.py:29 +msgid "Astronomical dusk" +msgstr "Anoitecer astronômico" + +#: django_celery_beat/choices.py:30 +msgid "Civil dusk" +msgstr "Anoitecer civil" + +#: django_celery_beat/choices.py:31 +msgid "Nautical dusk" +msgstr "Anoitecer náutico" + +#: django_celery_beat/choices.py:32 +msgid "Solar noon" +msgstr "Meio-dia solar" + +#: django_celery_beat/choices.py:33 +msgid "Sunrise" +msgstr "Nascer do sol" + +#: django_celery_beat/choices.py:34 +msgid "Sunset" +msgstr "Pôr do sol" + +#: django_celery_beat/models.py:70 +msgid "Solar Event" +msgstr "Evento Solar" + +#: django_celery_beat/models.py:71 +msgid "The type of solar event when the job should run" +msgstr "O tipo de evento solar quando o trabalho deve ser executado" + +#: django_celery_beat/models.py:76 +msgid "Latitude" +msgstr "Latitude" + +#: django_celery_beat/models.py:77 +msgid "Run the task when the event happens at this latitude" +msgstr "Executar a tarefa quando o evento ocorrer nesta latitude" + +#: django_celery_beat/models.py:83 +msgid "Longitude" +msgstr "Longitude" + +#: django_celery_beat/models.py:84 +msgid "Run the task when the event happens at this longitude" +msgstr "Executar a tarefa quando o evento ocorrer nesta longitude" + +#: django_celery_beat/models.py:91 +msgid "solar event" +msgstr "evento solar" + +#: django_celery_beat/models.py:92 +msgid "solar events" +msgstr "eventos solares" + +#: django_celery_beat/models.py:132 +msgid "Number of Periods" +msgstr "Número de Períodos" + +#: django_celery_beat/models.py:134 +msgid "Number of interval periods to wait before running the task again" +msgstr "Número de períodos de intervalo para esperar antes de executar a tarefa novamente" + +#: django_celery_beat/models.py:141 +msgid "Interval Period" +msgstr "Período de Intervalo" + +#: django_celery_beat/models.py:142 +msgid "The type of period between task runs (Example: days)" +msgstr "O tipo de período entre execuções de tarefas (Exemplo: dias)" + +#: django_celery_beat/models.py:148 +msgid "interval" +msgstr "intervalo" + +#: django_celery_beat/models.py:149 +msgid "intervals" +msgstr "intervalos" + +#: django_celery_beat/models.py:169 +msgid "Minute (integer from 0-59)" +msgstr "Minuto (inteiro de 0-59)" + +#: django_celery_beat/models.py:171 +msgid "Hour (integer from 0-23)" +msgstr "Hora (inteiro de 0-23)" + +#: django_celery_beat/models.py:173 +msgid "Day of the week (0-6, 0 is Sunday, or 'mon', 'tue', etc.)" +msgstr "Dia da semana (0-6, 0 é Domingo, ou 'mon', 'tue', etc.)" + +#: django_celery_beat/models.py:176 +msgid "Day of the month (1-31)" +msgstr "Dia do mês (1-31)" + +#: django_celery_beat/models.py:178 +msgid "Month of the year (1-12)" +msgstr "Mês do ano (1-12)" + +#: django_celery_beat/models.py:180 +msgid "Timezone" +msgstr "Fuso horário" + +#: django_celery_beat/models.py:209 +msgid "The human-readable timezone name that this schedule will follow (e.g. 'Europe/Berlin')" +msgstr "O nome legível do fuso horário que este agendamento seguirá (ex. 'America/Sao_Paulo')" + +#: django_celery_beat/models.py:233 +msgid "crontab" +msgstr "crontab" + +#: django_celery_beat/models.py:234 +msgid "crontabs" +msgstr "crontabs" + +#: django_celery_beat/models.py:254 +msgid "Name" +msgstr "Nome" + +#: django_celery_beat/models.py:255 +msgid "Event" +msgstr "Evento" + +#: django_celery_beat/models.py:301 +msgid "Clocked Time" +msgstr "Hora Programada" + +#: django_celery_beat/models.py:308 +msgid "clocked schedule" +msgstr "agendamento programado" + +#: django_celery_beat/models.py:309 +msgid "clocked schedules" +msgstr "agendamentos programados" + +#: django_celery_beat/models.py:407 +msgid "seconds" +msgstr "segundos" + +#: django_celery_beat/models.py:412 +msgid "positional arguments" +msgstr "argumentos posicionais" + +#: django_celery_beat/models.py:414 +msgid "JSON encoded positional arguments (Example: [\"arg1\", \"arg2\"])" +msgstr "Argumentos posicionais codificados em JSON (Exemplo: [\"arg1\", \"arg2\"])" + +#: django_celery_beat/models.py:419 +msgid "keyword arguments" +msgstr "argumentos de palavras-chave" + +#: django_celery_beat/models.py:421 +msgid "JSON encoded keyword arguments (Example: {\"argument\": \"value\"})" +msgstr "Argumentos de palavras-chave codificados em JSON (Exemplo: {\"argument\": \"value\"})" + +#: django_celery_beat/models.py:426 +msgid "queue" +msgstr "fila" + +#: django_celery_beat/models.py:430 +msgid "exchange" +msgstr "exchange" + +#: django_celery_beat/models.py:434 +msgid "routing key" +msgstr "chave de roteamento" + +#: django_celery_beat/models.py:438 +msgid "headers" +msgstr "cabeçalhos" + +#: django_celery_beat/models.py:440 +msgid "JSON encoded message headers for the AMQP message." +msgstr "Cabeçalhos de mensagem codificados em JSON para a mensagem AMQP." + +#: django_celery_beat/models.py:444 +msgid "priority" +msgstr "prioridade" + +#: django_celery_beat/models.py:446 +msgid "Priority Number between 0 and 255. Supported by: RabbitMQ." +msgstr "Número de prioridade entre 0 e 255. Suportado por: RabbitMQ." + +#: django_celery_beat/models.py:453 +msgid "expires" +msgstr "expira" + +#: django_celery_beat/models.py:455 +msgid "Datetime after which the schedule will no longer trigger the task to run" +msgstr "Data e hora após as quais o agendamento não acionará mais a execução da tarefa" + +#: django_celery_beat/models.py:460 +msgid "expire seconds" +msgstr "segundos de expiração" + +#: django_celery_beat/models.py:462 +msgid "Timedelta with seconds which the schedule will no longer trigger the task to run" +msgstr "Timedelta em segundos após o qual o agendamento não acionará mais a execução da tarefa" + +#: django_celery_beat/models.py:467 +msgid "one-off task" +msgstr "tarefa única" + +#: django_celery_beat/models.py:469 +msgid "If True, the schedule will only run the task a single time" +msgstr "Se True, o agendamento executará a tarefa apenas uma vez" + +#: django_celery_beat/models.py:473 +msgid "start datetime" +msgstr "data e hora de início" + +#: django_celery_beat/models.py:475 +msgid "Datetime when the schedule should begin triggering the task to run" +msgstr "Data e hora em que o agendamento deve começar a acionar a execução da tarefa" + +#: django_celery_beat/models.py:480 +msgid "enabled" +msgstr "habilitado" + +#: django_celery_beat/models.py:482 +msgid "Set to False to disable the schedule" +msgstr "Definir como False para desabilitar o agendamento" + +#: django_celery_beat/models.py:486 +msgid "last run at" +msgstr "última execução em" + +#: django_celery_beat/models.py:488 +msgid "Datetime that the schedule last triggered the task to run. Reset to None if enabled is set to False." +msgstr "Data e hora em que o agendamento acionou pela última vez a execução da tarefa. Redefinido para None se enabled for definido como False." + +#: django_celery_beat/models.py:493 +msgid "total run count" +msgstr "contagem total de execuções" + +#: django_celery_beat/models.py:495 +msgid "Running count of how many times the schedule has triggered the task" +msgstr "Contagem de quantas vezes o agendamento acionou a tarefa" + +#: django_celery_beat/models.py:499 +msgid "datetime changed" +msgstr "data e hora alterada" + +#: django_celery_beat/models.py:501 +msgid "Datetime that this PeriodicTask was last modified" +msgstr "Data e hora em que esta PeriodicTask foi modificada pela última vez" + +#: django_celery_beat/models.py:505 +msgid "description" +msgstr "descrição" + +#: django_celery_beat/models.py:507 +msgid "Detailed description about the details of this Periodic Task" +msgstr "Descrição detalhada sobre os detalhes desta Tarefa Periódica" + +#: django_celery_beat/models.py:755 +msgid "Only one schedule can be selected: Interval, Crontab, Solar, or Clocked" +msgstr "Apenas um agendamento pode ser selecionado: Intervalo, Crontab, Solar ou Programado" + +#: django_celery_beat/models.py:759 +msgid "One of interval, crontab, solar, or clocked must be set." +msgstr "Um de intervalo, crontab, solar ou programado deve ser definido." + +#: django_celery_beat/models.py:785 +msgid "periodic task" +msgstr "tarefa periódica" + +#: django_celery_beat/models.py:786 +msgid "periodic tasks" +msgstr "tarefas periódicas" + +#: django_celery_beat/wagtail_hooks.py:217 +msgid "enabled,disabled" +msgstr "habilitada,desabilitada" + +#: django_celery_beat/wagtail_hooks.py:224 +msgid "Periodic tasks" +msgstr "Tarefas periódicas" + +#: journal/models.py:27 +msgid "Site ID" +msgstr "ID do Site" + +#: journal/models.py:29 +msgid "Short title" +msgstr "Título abreviado" + +#: journal/models.py:31 +msgid "Journal ISSN" +msgstr "ISSN da Revista" + +#: journal/models.py:33 +msgid "Online Journal ISSN" +msgstr "ISSN da Revista Online" + +#: journal/models.py:36 +msgid "ISSN SciELO" +msgstr "ISSN SciELO" + +#: journal/models.py:38 +msgid "ISSN SciELO Legacy" +msgstr "ISSN SciELO Legacy" + +#: journal/models.py:41 +msgid "Subject descriptors" +msgstr "Descritores de assunto" + +#: journal/models.py:46 +msgid "Purpose" +msgstr "Propósito" + +#: journal/models.py:49 +msgid "Sponsorship" +msgstr "Patrocínio" + +#: journal/models.py:52 +msgid "Mission" +msgstr "Missão" + +#: journal/models.py:55 +msgid "Index at" +msgstr "Indexado em" + +#: journal/models.py:58 +msgid "Availability" +msgstr "Disponibilidade" + +#: journal/models.py:61 +msgid "Summary form" +msgstr "Forma de resumo" + +#: journal/models.py:64 +msgid "Standard" +msgstr "Padrão" + +#: journal/models.py:67 +msgid "Alphabet title" +msgstr "Título alfabético" + +#: journal/models.py:69 +msgid "Print title" +msgstr "Título impresso" + +#: journal/models.py:72 +msgid "Short title (slug)" +msgstr "Título abreviado (slug)" + +#: journal/models.py:76 +#: markup_doc/models.py:40 +msgid "Title" +msgstr "Título" + +#: journal/models.py:81 +msgid "Subtitle" +msgstr "Subtítulo" + +#: journal/models.py:86 +msgid "Next title" +msgstr "Título seguinte" + +#: journal/models.py:90 +msgid "Previous title" +msgstr "Título anterior" + +#: journal/models.py:94 +msgid "Control number" +msgstr "Número de controle" + +#: journal/models.py:97 +msgid "Publisher name" +msgstr "Nome do editor" + +#: journal/models.py:99 +msgid "Publisher country" +msgstr "País do editor" + +#: journal/models.py:102 +msgid "Publisher state" +msgstr "Estado do editor" + +#: journal/models.py:105 +msgid "Publisher city" +msgstr "Cidade do editor" + +#: journal/models.py:108 +msgid "Publisher address" +msgstr "Endereço do editor" + +#: journal/models.py:111 +msgid "Publication level" +msgstr "Nível de publicação" + +#: journal/models.py:115 +msgid "Email" +msgstr "E-mail" + +#: journal/models.py:119 +msgid "URL online submission" +msgstr "URL de submissão online" + +#: journal/models.py:122 +msgid "URL home page" +msgstr "URL da página inicial" + +#: journal/models.py:146 +msgid "Journal" +msgstr "Revista" + +#: journal/models.py:147 +msgid "Journals" +msgstr "Revistas" + +#: journal/models.py:162 +msgid "Order" +msgstr "Ordem" + +#: journal/models.py:174 +msgid "Editor" +msgstr "Editor" + +#: journal/models.py:175 +msgid "Editors" +msgstr "Editores" + +#: journal/wagtail_hooks.py:57 +msgid "Journal and Editor" +msgstr "Revista e Editor" + +#: location/models.py:11 +msgid "Country Name" +msgstr "Nome do País" + +#: location/models.py:12 +msgid "ACR3" +msgstr "ACR3" + +#: location/models.py:13 +msgid "ACR2" +msgstr "ACR2" + +#: location/models.py:23 +msgid "Country" +msgstr "País" + +#: location/models.py:24 +msgid "Countries" +msgstr "Países" + +#: location/models.py:36 +msgid "State name" +msgstr "Nome do estado" + +#: location/models.py:37 +msgid "ACR 2 (state)" +msgstr "ACR 2 (estado)" + +#: location/models.py:48 +msgid "State" +msgstr "Estado" + +#: location/models.py:49 +msgid "States" +msgstr "Estados" + +#: location/models.py:61 +msgid "City name" +msgstr "Nome da cidade" + +#: location/models.py:75 +msgid "City" +msgstr "Cidade" + +#: location/models.py:76 +msgid "Cities" +msgstr "Cidades" + +#: location/wagtail_hooks.py:55 +msgid "Location" +msgstr "Localização" + +#: markup_doc/choices.py:11 +msgid "Document without content" +msgstr "Documento sem conteúdo" + +#: markup_doc/choices.py:12 +msgid "Structured document without references" +msgstr "Documento estruturado sem referências" + +#: markup_doc/choices.py:13 +msgid "Structured document" +msgstr "Documento estruturado" + +#: markup_doc/choices.py:14 +msgid "Structured document and references" +msgstr "Documento estruturado e referências" + +#: markup_doc/models.py:33 +msgid "DOI" +msgstr "DOI" + +#: markup_doc/models.py:56 +msgid "Rich Title" +msgstr "Título Rico" + +#: markup_doc/models.py:58 +msgid "Clean Title" +msgstr "Título Limpo" + +#: markup_doc/models.py:97 +msgid "Document" +msgstr "Documento" + +#: markup_doc/models.py:98 +#: markup_doc/wagtail_hooks.py:94 +msgid "Documents" +msgstr "Documentos" + +#: markup_doc/models.py:130 +msgid "Type" +msgstr "Tipo" + +#: markup_doc/models.py:137 +msgid "Paragraph" +msgstr "Parágrafo" + +#: markup_doc/models.py:138 +msgid "Paragraphs" +msgstr "Parágrafos" + +#: markup_doc/models.py:218 +msgid "Section title" +msgstr "Título da seção" + +#: markup_doc/models.py:222 +msgid "Section code" +msgstr "Código da seção" + +#: markup_doc/models.py:245 +msgid "Section" +msgstr "Seção" + +#: markup_doc/models.py:246 +msgid "Sections" +msgstr "Seções" + +#: markup_doc/models.py:279 +msgid "Figure" +msgstr "Figura" + +#: markup_doc/models.py:280 +msgid "Figures" +msgstr "Figuras" + +#: markup_doc/models.py:306 +msgid "Table" +msgstr "Tabela" + +#: markup_doc/models.py:307 +msgid "Tables" +msgstr "Tabelas" + +#: markup_doc/models.py:322 +msgid "Supplementary Material" +msgstr "Material Suplementar" + +#: markup_doc/models.py:323 +msgid "Supplementary Materials" +msgstr "Materiais Suplementares" + +#: markup_doc/models.py:336 +msgid "Document ID" +msgstr "ID do Documento" + +#: markup_doc/models.py:340 +msgid "Document ID type" +msgstr "Tipo de ID do documento" + +#: markup_doc/models.py:341 +msgid "Publisher ID" +msgstr "ID do Editor" + +#: markup_doc/models.py:342 +msgid "Pub Acronym" +msgstr "Acrônimo da Publicação" + +#: markup_doc/models.py:343 +msgid "Vol" +msgstr "Vol" + +#: markup_doc/models.py:344 +msgid "Suppl Vol" +msgstr "Supl Vol" + +#: markup_doc/models.py:345 +msgid "Num" +msgstr "Núm" + +#: markup_doc/models.py:346 +msgid "Suppl Num" +msgstr "Supl Núm" + +#: markup_doc/models.py:346 +msgid "Isid Part" +msgstr "Parte Isid" + +#: markup_doc/models.py:347 +msgid "Dateiso" +msgstr "Data ISO" + +#: markup_doc/models.py:348 +msgid "Month/Season" +msgstr "Mês/Estação" + +#: markup_doc/models.py:349 +msgid "First Page" +msgstr "Primeira Página" + +#: markup_doc/models.py:350 +msgid "@Seq" +msgstr "@Seq" + +#: markup_doc/models.py:351 +msgid "Last Page" +msgstr "Última Página" + +#: markup_doc/models.py:352 +msgid "Elocation ID" +msgstr "ID de Elocation" + +#: markup_doc/models.py:353 +msgid "Order (In TOC)" +msgstr "Ordem (No Sumário)" + +#: markup_doc/models.py:354 +msgid "Pag count" +msgstr "Contagem de Páginas" + +#: markup_doc/models.py:355 +msgid "Doc Topic" +msgstr "Tópico do Documento" + +#: markup_doc/models.py:363 +msgid "Sps version" +msgstr "Versão SPS" + +#: markup_doc/models.py:364 +msgid "Artdate" +msgstr "Data do Artigo" + +#: markup_doc/models.py:365 +msgid "Ahpdate" +msgstr "Data AHP" + +#: markup_doc/models.py:370 +msgid "Document xml" +msgstr "XML do documento" + +#: markup_doc/models.py:374 +msgid "Text XML" +msgstr "Texto XML" + +#: markup_doc/models.py:513 +msgid "Details" +msgstr "Detalhes" + +#: markup_doc/wagtail_hooks.py:117 +msgid "Documents Markup" +msgstr "Marcação de Documentos" + +#: markup_doc/wagtail_hooks.py:130 +msgid "Carregar DOCX" +msgstr "Carregar DOCX" + +#: markup_doc/wagtail_hooks.py:148 +msgid "XML marcado" +msgstr "XML marcado" + +#: markup_doc/wagtail_hooks.py:189 +msgid "Modelo de Coleções" +msgstr "Modelo de Coleções" + +#: markup_doc/wagtail_hooks.py:209 +msgid "Modelo de Revistas" +msgstr "Modelo de Revistas" + +#: markup_doc/wagtail_hooks.py:240 +msgid "DOCX Files" +msgstr "Arquivos DOCX" + +#: model_ai/models.py:21 +msgid "No model" +msgstr "Sem modelo" + +#: model_ai/models.py:22 +msgid "Downloading model" +msgstr "Baixando modelo" + +#: model_ai/models.py:23 +msgid "Model downloaded" +msgstr "Modelo baixado" + +#: model_ai/models.py:24 +msgid "Download error" +msgstr "Erro no download" + +#: model_ai/models.py:34 +msgid "Hugging Face model name" +msgstr "Nome do modelo Hugging Face" + +#: model_ai/models.py:35 +msgid "Model file" +msgstr "Arquivo do modelo" + +#: model_ai/models.py:36 +msgid "Hugging Face token" +msgstr "Token do Hugging Face" + +#: model_ai/models.py:38 +msgid "Local model status" +msgstr "Status do modelo local" + +#: model_ai/models.py:44 +msgid "URL Markapi" +msgstr "URL Markapi" + +#: model_ai/models.py:49 +msgid "API KEY Gemini" +msgstr "Chave API Gemini" + +#: model_ai/models.py:67 model_ai/models.py:71 +msgid "Only one instance of LlamaModel is allowed." +msgstr "Apenas uma instância de LlamaModel é permitida." + +#: model_ai/wagtail_hooks.py:28 model_ai/wagtail_hooks.py:63 +msgid "Model name is required." +msgstr "O nome do modelo é obrigatório." + +#: model_ai/wagtail_hooks.py:32 model_ai/wagtail_hooks.py:67 +msgid "Model file is required." +msgstr "O arquivo do modelo é obrigatório." + +#: model_ai/wagtail_hooks.py:36 model_ai/wagtail_hooks.py:71 +msgid "Hugging Face token is required." +msgstr "O token do Hugging Face é obrigatório." + +#: model_ai/wagtail_hooks.py:42 +msgid "Model created and download started." +msgstr "Modelo criado e download iniciado." + +#: model_ai/wagtail_hooks.py:46 model_ai/wagtail_hooks.py:82 +msgid "API AI URL is required." +msgstr "A URL da API AI é obrigatória." + +#: model_ai/wagtail_hooks.py:50 +msgid "Model created, use API AI." +msgstr "Modelo criado, usar API AI." + +#: model_ai/wagtail_hooks.py:77 +msgid "Model updated and download started." +msgstr "Modelo atualizado e download iniciado." + +#: model_ai/wagtail_hooks.py:79 +msgid "Model updated and already downloaded." +msgstr "Modelo atualizado e já baixado." + +#: model_ai/wagtail_hooks.py:86 +msgid "Model updated, use API AI." +msgstr "Modelo atualizado, usar API AI." + +#: model_ai/wagtail_hooks.py:95 +msgid "AI LLM Model" +msgstr "Modelo LLM de IA" + +#: reference/models.py:15 +msgid "No reference" +msgstr "Sem referência" + +#: reference/models.py:16 +msgid "Creating reference" +msgstr "Criando referência" + +#: reference/models.py:17 +msgid "Reference ready" +msgstr "Referência pronta" + +#: reference/models.py:22 +msgid "Mixed Citation" +msgstr "Citação mista" + +#: reference/models.py:25 +msgid "Reference status" +msgstr "Status da referência" + +#: reference/models.py:33 +msgid "Cited Elements" +msgstr "Elementos Citados" + +#: reference/models.py:46 +msgid "Marked" +msgstr "Marcado" + +#: reference/models.py:47 +msgid "Marked XML" +msgstr "XML Marcado" + +#: reference/models.py:56 +msgid "Rating from 1 to 10" +msgstr "Classificação de 1 a 10" + +#: reference/wagtail_hooks.py:41 +msgid "Reference" +msgstr "Referência" + +#: tracker/choices.py:9 +msgid "error" +msgstr "erro" + +#: tracker/choices.py:10 +msgid "warning" +msgstr "aviso" + +#: tracker/choices.py:11 +msgid "info" +msgstr "informação" + +#: tracker/choices.py:12 +msgid "exception" +msgstr "exceção" + +#: tracker/choices.py:24 +msgid "To reprocess" +msgstr "Para reprocessar" + +#: tracker/choices.py:25 +msgid "To do" +msgstr "A fazer" + +#: tracker/choices.py:26 +msgid "Done" +msgstr "Concluído" + +#: tracker/choices.py:27 +msgid "Doing" +msgstr "Fazendo" + +#: tracker/choices.py:28 +msgid "Pending" +msgstr "Pendente" + +#: tracker/choices.py:29 +msgid "ignored" +msgstr "ignorado" + +#: tracker/choices.py:41 +msgid "XML Parsing Error" +msgstr "Erro de Análise XML" + +#: tracker/choices.py:42 +msgid "XML Validation Error" +msgstr "Erro de Validação XML" + +#: tracker/choices.py:43 +msgid "XML Conversion to DOCX Error" +msgstr "Erro de Conversão XML para DOCX" + +#: tracker/choices.py:44 +msgid "XML Conversion to HTML Error" +msgstr "Erro de Conversão XML para HTML" + +#: tracker/choices.py:45 +msgid "XML Conversion to PDF Error" +msgstr "Erro de Conversão XML para PDF" + +#: tracker/choices.py:46 +msgid "XML Conversion to TEX Error" +msgstr "Erro de Conversão XML para TEX" + +#: tracker/choices.py:47 +msgid "Unknown Error" +msgstr "Erro Desconhecido" + +#: tracker/models.py:28 +msgid "Error Type" +msgstr "Tipo de Erro" + +#: tracker/models.py:35 +msgid "Data" +msgstr "Dados" + +#: tracker/models.py:39 +msgid "Message" +msgstr "Mensagem" + +#: tracker/models.py:44 +msgid "Handled" +msgstr "Tratado" + +#: tracker/models.py:70 +msgid "XML Document Event" +msgstr "Evento de Documento XML" + +#: tracker/models.py:71 tracker/wagtail_hooks.py:16 +msgid "XML Document Events" +msgstr "Eventos de Documento XML" + +#: tracker/models.py:77 +msgid "Exception Type" +msgstr "Tipo de Exceção" + +#: tracker/models.py:78 +msgid "Exception Msg" +msgstr "Mensagem de Exceção" + +#: tracker/models.py:82 +msgid "Item" +msgstr "Item" + +#: tracker/models.py:88 xml_manager/wagtail_hooks.py:69 +msgid "Action" +msgstr "Ação" + +#: tracker/models.py:101 +msgid "General Event" +msgstr "Evento Geral" + +#: tracker/models.py:102 tracker/wagtail_hooks.py:47 +msgid "General Events" +msgstr "Eventos Gerais" + +#: tracker/wagtail_hooks.py:81 +msgid "Unexpected Events" +msgstr "Eventos Inesperados" + +#: users/admin.py:13 +msgid "Personal info" +msgstr "Informações pessoais" + +#: users/admin.py:15 +msgid "Permissions" +msgstr "Permissões" + +#: users/admin.py:26 +msgid "Important dates" +msgstr "Datas importantes" + +#: users/forms.py:11 +msgid "This username has already been taken." +msgstr "Este nome de usuário já foi utilizado." + +#: users/models.py:15 +msgid "Name of User" +msgstr "Nome do Usuário" + +#: xml_manager/models.py:9 +msgid "XML File" +msgstr "Arquivo XML" + +#: xml_manager/models.py:10 +msgid "Upload an XML file for processing." +msgstr "Enviar um arquivo XML para processamento." + +#: xml_manager/models.py:16 +msgid "Validation File" +msgstr "Arquivo de Validação" + +#: xml_manager/models.py:22 +msgid "Exceptions File" +msgstr "Arquivo de Exceções" + +#: xml_manager/models.py:26 xml_manager/models.py:61 xml_manager/models.py:93 +msgid "Uploaded At" +msgstr "Enviado Em" + +#: xml_manager/models.py:27 +msgid "The date and time when the file was uploaded." +msgstr "A data e hora em que o arquivo foi enviado." + +#: xml_manager/models.py:38 xml_manager/models.py:43 xml_manager/models.py:82 +#: xml_manager/wagtail_hooks.py:56 xml_manager/wagtail_hooks.py:60 +msgid "XML Document" +msgstr "Documento XML" + +#: xml_manager/models.py:39 xml_manager/wagtail_hooks.py:57 +msgid "XML Documents" +msgstr "Documentos XML" + +#: xml_manager/models.py:46 +msgid "PDF File" +msgstr "Arquivo PDF" + +#: xml_manager/models.py:50 +msgid "DOCX File" +msgstr "Arquivo DOCX" + +#: xml_manager/models.py:53 +msgid "Intermediate DOCX file generated during PDF creation" +msgstr "Arquivo DOCX intermediário gerado durante a criação do PDF" + +#: xml_manager/models.py:59 xml_manager/models.py:91 +msgid "Language code or name" +msgstr "Código ou nome do idioma" + +#: xml_manager/models.py:67 xml_manager/wagtail_hooks.py:77 +#: xml_manager/wagtail_hooks.py:81 +msgid "XML Document PDF" +msgstr "PDF de Documento XML" + +#: xml_manager/models.py:68 xml_manager/wagtail_hooks.py:78 +msgid "XML Document PDFs" +msgstr "PDFs de Documento XML" + +#: xml_manager/models.py:85 +msgid "HTML File" +msgstr "Arquivo HTML" + +#: xml_manager/models.py:99 xml_manager/wagtail_hooks.py:98 +#: xml_manager/wagtail_hooks.py:102 +msgid "XML Document HTML" +msgstr "HTML de Documento XML" + +#: xml_manager/models.py:100 xml_manager/wagtail_hooks.py:99 +msgid "XML Document HTMLs" +msgstr "HTMLs de Documento XML" + +#: xml_manager/wagtail_hooks.py:66 +msgid "Validation file" +msgstr "Arquivo de validação" + +#: xml_manager/wagtail_hooks.py:67 +msgid "Exceptions file" +msgstr "Arquivo de exceções" + +#: xml_manager/wagtail_hooks.py:118 +msgid "XML Processor" +msgstr "Processador XML" + + +#: menu items +msgid "XML Files" +msgstr "Arquivos XML" + +#: menu items +msgid "Tarefas" +msgstr "Tarefas" + +#: markup_doc/wagtail_hooks.py +msgid "You must first select a collection." +msgstr "Você deve primeiro selecionar uma coleção." + +#: markup_doc/wagtail_hooks.py +msgid "Wait a moment, there are no Journal elements yet." +msgstr "Aguarde um momento, ainda não existem elementos em Journal." + +#: markup_doc/wagtail_hooks.py +msgid "Synchronizing journals from the API, please wait a moment..." +msgstr "Sincronizando revistas da API, aguarde um momento..." \ No newline at end of file diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..9190891 --- /dev/null +++ b/manage.py @@ -0,0 +1,30 @@ +import os +import sys +from pathlib import Path + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") + + try: + from django.core.management import execute_from_command_line + except ImportError: + # The above import may fail for some other reason. Ensure that the + # issue is really that Django is missing to avoid masking other + # exceptions on Python 2. + try: + import django # noqa + except ImportError: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) + + raise + + # This allows easy placement of apps within the interior + # core directory. + current_path = Path(__file__).parent.resolve() + sys.path.append(str(current_path / "core")) + + execute_from_command_line(sys.argv) \ No newline at end of file diff --git a/manuscripts/__init__.py b/manuscripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/manuscripts/api/__init__.py b/manuscripts/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/manuscripts/api/v1/__init__.py b/manuscripts/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/manuscripts/api/v1/serializers.py b/manuscripts/api/v1/serializers.py new file mode 100644 index 0000000..61f0bfe --- /dev/null +++ b/manuscripts/api/v1/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from manuscripts.models.article import Article + + +class ArticleSerializer(serializers.ModelSerializer): + class Meta: + model = Article + fields = "__all__" diff --git a/manuscripts/api/v1/views.py b/manuscripts/api/v1/views.py new file mode 100755 index 0000000..bd77c27 --- /dev/null +++ b/manuscripts/api/v1/views.py @@ -0,0 +1,39 @@ +import json + +from django.http import JsonResponse +from rest_framework.mixins import CreateModelMixin +from rest_framework.permissions import IsAuthenticated +from rest_framework.viewsets import GenericViewSet + +from manuscripts.api.v1.serializers import ArticleSerializer + +# Create your views here. + +class ArticleViewSet( + GenericViewSet, # generic view functionality + CreateModelMixin, # handles POSTs +): + serializer_class = ArticleSerializer + permission_classes = [IsAuthenticated] + http_method_names = [ + "post", + ] + + def create(self, request, *args, **kwargs): + return self.api_article(request) + + def api_article(self, request): + try: + data = json.loads(request.body) + _text = data.get('text') + _metadata = data.get('metadata') + + response_data = { + 'message': 'Article marking API is deprecated.', + } + except json.JSONDecodeError: + response_data = { + 'error': 'Error processing' + } + + return JsonResponse(response_data) diff --git a/manuscripts/apps.py b/manuscripts/apps.py new file mode 100644 index 0000000..606ef87 --- /dev/null +++ b/manuscripts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ManuscriptsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "manuscripts" diff --git a/manuscripts/artifacts.py b/manuscripts/artifacts.py new file mode 100644 index 0000000..f0e8066 --- /dev/null +++ b/manuscripts/artifacts.py @@ -0,0 +1,207 @@ +import ipaddress +import os +import shutil +import socket +import tempfile +import zipfile +from pathlib import PurePosixPath +from urllib.parse import urlparse +from urllib.request import HTTPRedirectHandler, Request, build_opener + +from django.core.files.base import ContentFile +from django.utils import timezone + +from .choices import ArtifactType, EventStatus +from .models.article import ArticleArtifact +from .models.processing import ProcessingEvent +from .utils.helpers import checksum_bytes, json_safe, safe_archive_members + + +class NoRedirectHandler(HTTPRedirectHandler): + def redirect_request(self, req, fp, code, msg, headers, newurl): + raise ValueError("Redirecionamentos não são permitidos para assets externos.") + + +def is_safe_external_url(url): + parsed = urlparse(url) + if parsed.scheme not in {"http", "https"} or not parsed.hostname: + return False + try: + addresses = socket.getaddrinfo(parsed.hostname, parsed.port or 443) + except socket.gaierror: + return False + for address in addresses: + ip = ipaddress.ip_address(address[4][0]) + if not ip.is_global: + return False + return True + + +def save_artifact(processing, artifact_type, name, content, article=None, metadata=None, original_path="", source_url="", structure=None): + if artifact_type == ArtifactType.ASSET and not original_path: + original_path = os.path.basename(name) + current = ArticleArtifact.objects.filter( + processing=processing, article=article, artifact_type=artifact_type, is_current=True + ) + if artifact_type == ArtifactType.ASSET: + current = current.filter(original_path=original_path) + version = (current.order_by("-version").values_list("version", flat=True).first() or 0) + 1 + current.update(is_current=False) + artifact = ArticleArtifact.objects.create( + processing=processing, + article=article, + artifact_type=artifact_type, + version=version, + metadata=json_safe(metadata or {}), + original_path=original_path, + source_url=source_url, + structure=structure, + checksum=checksum_bytes(content), + file_size_bytes=len(content), + ) + artifact.file.save(os.path.basename(name), ContentFile(content), save=True) + return artifact + + +def save_path_artifact(processing, artifact_type, path, article=None, metadata=None, structure=None): + with open(path, "rb") as source: + return save_artifact( + processing, + artifact_type, + os.path.basename(path), + source.read(), + article, + metadata, + structure=structure, + ) + + +def current_xml(processing, article): + artifact = ArticleArtifact.objects.filter( + processing=processing, article=article, artifact_type=ArtifactType.XML, is_current=True + ).first() + if not artifact: + raise ValueError(f"Artigo {article} não possui XML neste processamento.") + return artifact + + +def article_asset_url_map(processing, article): + return { + PurePosixPath(artifact.original_path or artifact.file.name).name: artifact.file.url + for artifact in ArticleArtifact.objects.filter( + processing=processing, + article=article, + artifact_type=ArtifactType.ASSET, + is_current=True, + ) + } + + +def article_assets_dir(processing, article): + assets = list( + ArticleArtifact.objects.filter( + processing=processing, + article=article, + artifact_type=ArtifactType.ASSET, + is_current=True, + ) + ) + if not assets: + return None + + tmp_dir = tempfile.mkdtemp(prefix="scielo_tools_pdf_assets_") + for artifact in assets: + original_name = PurePosixPath(artifact.original_path or artifact.file.name).name + target = os.path.join(tmp_dir, original_name) + src = artifact.file.path + if not os.path.exists(target): + try: + os.symlink(src, target) + except OSError: + shutil.copy2(src, target) + return tmp_dir + + +def resolve_article_assets(processing, article, references): + local_assets = ArticleArtifact.objects.filter( + processing=processing, artifact_type=ArtifactType.ASSET + ) + for reference in references: + parsed_name = PurePosixPath(reference.split("?", 1)[0]).name + local = next( + ( + asset + for asset in local_assets + if PurePosixPath(asset.original_path).name == parsed_name + ), + None, + ) + if local: + if local.article_id and local.article_id != article.pk: + with local.file.open("rb") as source: + save_artifact( + processing, + ArtifactType.ASSET, + local.original_path or os.path.basename(local.file.name), + source.read(), + article, + original_path=local.original_path, + source_url=local.source_url, + ) + else: + local.article = article + local.save(update_fields=["article", "updated"]) + continue + if not is_safe_external_url(reference): + ProcessingEvent.objects.create( + processing=processing, + article=article, + status=EventStatus.FAILED, + message=f"Asset ausente ou URL externa bloqueada: {reference}", + details={"severity": "warning", "asset": reference}, + completed_at=timezone.now(), + ) + continue + try: + request = Request(reference, headers={"User-Agent": "SciELO Tools/1.0"}) + with build_opener(NoRedirectHandler).open(request, timeout=10) as response: + content = response.read(25 * 1024 * 1024 + 1) + if len(content) > 25 * 1024 * 1024: + continue + save_artifact( + processing, + ArtifactType.ASSET, + parsed_name or "asset", + content, + article, + source_url=reference, + ) + except Exception as exc: + ProcessingEvent.objects.create( + processing=processing, + article=article, + status=EventStatus.FAILED, + message=f"Não foi possível baixar o asset: {reference}", + details={"severity": "warning", "asset": reference, "error": str(exc)}, + completed_at=timezone.now(), + ) + continue + + +def extract_docx_assets(processing, source_artifact, article): + try: + with zipfile.ZipFile(source_artifact.file.path) as archive: + for member in safe_archive_members(archive): + path = PurePosixPath(member.filename) + if path.parts[:2] != ("word", "media"): + continue + save_artifact( + processing, + ArtifactType.ASSET, + path.name, + archive.read(member), + article, + original_path=path.name, + ) + except zipfile.BadZipFile: + return diff --git a/manuscripts/blocks.py b/manuscripts/blocks.py new file mode 100644 index 0000000..465e9e5 --- /dev/null +++ b/manuscripts/blocks.py @@ -0,0 +1,132 @@ +from django.utils.translation import gettext_lazy as _ +from wagtail.blocks import ChoiceBlock, StreamBlock, StructBlock, TextBlock +from wagtail.images.blocks import ImageChooserBlock + +from core.choices import LANGUAGE + +from .choices import front_labels + + +class ParagraphWithLanguageBlock(StructBlock): + label = ChoiceBlock(choices=front_labels, required=False, label=_("Label")) + language = ChoiceBlock(choices=LANGUAGE, required=False, label=_("Language")) + paragraph = TextBlock(required=False, label=_("Title")) + + class Meta: + label = _("Paragraph with Language") + + +class ParagraphBlock(StructBlock): + label = ChoiceBlock(choices=front_labels, required=False, label=_("Label")) + paragraph = TextBlock(required=False, label=_("Paragraph")) + + class Meta: + label = _("Paragraph") + + +class CompoundParagraphBlock(StructBlock): + label = ChoiceBlock(choices=front_labels, required=False, label=_("Label")) + eid = TextBlock(required=False, label=_("Equation id")) + content = StreamBlock( + [ + ("text", TextBlock(label=_("Text"))), + ("formula", TextBlock(label=_("Formula"))), + ], + label=_("Content"), + required=True, + ) + + class Meta: + label = _("Compound paragraph") + + +class ImageBlock(StructBlock): + label = ChoiceBlock(choices=front_labels, required=False, label=_("Label")) + figid = TextBlock(required=False, label=_("Fig id")) + figlabel = TextBlock(required=False, label=_("Fig label")) + title = TextBlock(required=False, label=_("Title")) + alttext = TextBlock(required=False, label=_("Alt text")) + image = ImageChooserBlock(required=True) + + class Meta: + label = _("Image") + + +class TableBlock(StructBlock): + label = ChoiceBlock(choices=front_labels, required=False, label=_("Label")) + tabid = TextBlock(required=False, label=_("Table id")) + tablabel = TextBlock(required=False, label=_("Table label")) + title = TextBlock(required=False, label=_("Title")) + content = TextBlock(required=False, label=_("Content")) + + class Meta: + label = _("Table") + + +class AuthorParagraphBlock(ParagraphBlock): + surname = TextBlock(required=False, label=_("Surname")) + given_names = TextBlock(required=False, label=_("Given names")) + orcid = TextBlock(required=False, label=_("Orcid")) + affid = TextBlock(required=False, label=_("Aff id")) + char = TextBlock(required=False, label=_("Char link")) + + class Meta: + label = _("Author Paragraph") + + +class AffParagraphBlock(ParagraphBlock): + affid = TextBlock(required=False, label=_("Aff id")) + text_aff = TextBlock(required=False, label=_("Full text Aff")) + char = TextBlock(required=False, label=_("Char link")) + orgname = TextBlock(required=False, label=_("Orgname")) + orgdiv2 = TextBlock(required=False, label=_("Orgdiv2")) + orgdiv1 = TextBlock(required=False, label=_("Orgdiv1")) + zipcode = TextBlock(required=False, label=_("Zipcode")) + city = TextBlock(required=False, label=_("City")) + state = TextBlock(required=False, label=_("State")) + country = TextBlock(required=False, label=_("Country")) + code_country = TextBlock(required=False, label=_("Code country")) + original = TextBlock(required=False, label=_("Original")) + + class Meta: + label = _("Aff Paragraph") + + +class RefNameBlock(StructBlock): + surname = TextBlock(required=False, label=_("Surname")) + given_names = TextBlock(required=False, label=_("Given names")) + + +class RefParagraphBlock(ParagraphBlock): + reftype = TextBlock(required=False, label=_("Ref type")) + refid = TextBlock(required=False, label=_("Ref id")) + authors = StreamBlock( + [ + ("Author", RefNameBlock()), + ], + label=_("Authors"), + required=False, + ) + date = TextBlock(required=False, label=_("Date")) + title = TextBlock(required=False, label=_("Title")) + chapter = TextBlock(required=False, label=_("Chapter")) + edition = TextBlock(required=False, label=_("Edition")) + source = TextBlock(required=False, label=_("Source")) + vol = TextBlock(required=False, label=_("Vol")) + issue = TextBlock(required=False, label=_("Issue")) + pages = TextBlock(required=False, label=_("Pages")) + fpage = TextBlock(required=False, label=_("First page")) + lpage = TextBlock(required=False, label=_("Last page")) + doi = TextBlock(required=False, label=_("DOI")) + access_id = TextBlock(required=False, label=_("Access id")) + degree = TextBlock(required=False, label=_("Degree")) + organization = TextBlock(required=False, label=_("Organization")) + location = TextBlock(required=False, label=_("Location")) + org_location = TextBlock(required=False, label=_("Org location")) + num_pages = TextBlock(required=False, label=_("Num pages")) + uri = TextBlock(required=False, label=_("Uri")) + version = TextBlock(required=False, label=_("Version")) + access_date = TextBlock(required=False, label=_("Access date")) + + class Meta: + label = _("Ref Paragraph") diff --git a/manuscripts/choices.py b/manuscripts/choices.py new file mode 100644 index 0000000..9dfc464 --- /dev/null +++ b/manuscripts/choices.py @@ -0,0 +1,130 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ + +front_labels = [ + ("", ""), + ("", ""), + ("", ""), + ("", ""), + ("", ""), + ("", ""), + ("", ""), + ("", ""), + ("", ""), + ("", ""), + ("", ""), + ("", ""), + ("", ""), + ("", ""), + ("", ""), + ("

", "

"), + ("", ""), + ("", ""), + ("", ""), + ("

", "
"), + ("", ""), + ("", "<title>"), + ("<trans-abstract>", "<trans-abstract>"), + ("<trans-title>", "<trans-title>"), + ("<translate-front>", "<translate-front>"), + ("<translate-body>", "<translate-body>"), + ("<disp-formula>", "<disp-formula>"), + ("<inline-formula>", "<inline-formula>"), + ("<formula>", "<formula>"), +] + +order_labels = { + "<article-id>": {"pos": 1, "next": "<subject>"}, + "<subject>": {"pos": 2, "next": "<article-title>"}, + "<article-title>": {"pos": 3, "next": "<trans-title>", "lan": True}, + "<trans-title>": {"size": 14, "bold": True, "lan": True, "next": "<contrib>"}, + "<contrib>": {"reset": True, "size": 12, "next": "<aff>"}, + "<aff>": { + "reset": True, + "size": 12, + }, + "<abstract>": {"size": 12, "bold": True, "lan": True, "next": "<p>"}, + "<p>": {"size": 12, "next": "<p>", "repeat": True}, + "<trans-abstract>": {"size": 12, "bold": True, "lan": True, "next": "<p>"}, + "<kwd-group>": { + "size": 12, + "regex": r"(?i)(palabra.*clave.*:|keyword.*:)", + }, + "<history>": { + "size": 12, + "regex": r"\d{2}/\d{2}/\d{4}", + }, + "<corresp>": { + "size": 12, + "regex": r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", + }, + "<sec>": {"size": 16, "bold": True, "next": None}, + "<sub-sec>": {"size": 12, "italic": True, "next": None}, + "<sub-sec-2>": {"size": 14, "bold": True, "next": None}, +} + +order_labels_body = { + "<sec>": { + "size": 16, + "bold": True, + }, + "<sub-sec>": { + "size": 12, + "italic": True, + }, + "<p>": { + "size": 12, + }, +} + + +class ProcessStatus(models.TextChoices): + AWAITING_REVIEW = "awaiting_review", _("Awaiting review") + PENDING = "pending", _("Pending") + PROCESSING = "processing", _("Processing") + COMPLETED = "completed", _("Completed") + FAILED = "failed", _("Failed") + PARTIAL = "partial", _("Partial") + CANCELLED = "cancelled", _("Cancelled") + + +class InputType(models.TextChoices): + UNKNOWN = "unknown", _("Unidentified") + DOCUMENT = "document", _("Document") + XML = "xml", _("XML SPS") + SPS_PACKAGE = "sps_package", _("SPS Package") + SOURCE_PACKAGE = "source_package", _("Source package") + AMBIGUOUS_ZIP = "ambiguous_zip", _("Ambiguous ZIP package") + + +class ProcessingAction(models.TextChoices): + CITATION_MARKUP = "citation_markup", _("Mark citations") + XML_GENERATION = "xml_generation", _("Generate XML") + XML_VALIDATION = "xml_validation", _("Validate XML") + SPS_PACKAGE_VALIDATION = "sps_package_validation", _("Validate SPS package") + SPS_PACKAGE_GENERATION = "sps_package_generation", _("Generate SPS package") + HTML_GENERATION = "html_generation", _("Generate HTML") + PDF_GENERATION = "pdf_generation", _("Generate PDF") + + +class ArtifactType(models.TextChoices): + INPUT = "input", _("Uploaded file") + SOURCE_DOCUMENT = "source_document", _("Original document") + MARKED_DOCUMENT = "marked_document", _("Document with marked citations") + STRUCTURE = "structure", _("XML structure") + XML = "xml", _("XML SPS") + ASSET = "asset", _("Asset") + SPS_PACKAGE = "sps_package", _("SPS Package") + VALIDATION_REPORT = "validation_report", _("Validation report") + VALIDATION_EXCEPTIONS = "validation_exceptions", _("Validation exceptions") + HTML = "html", _("HTML") + PDF = "pdf", _("PDF") + INTERMEDIATE_DOCUMENT = "intermediate_document", _("Intermediate document") + + +class EventStatus(models.TextChoices): + PENDING = "pending", _("Pending") + RUNNING = "running", _("Running") + COMPLETED = "completed", _("Completed") + FAILED = "failed", _("Failed") + SKIPPED = "skipped", _("Skipped") diff --git a/manuscripts/controller.py b/manuscripts/controller.py new file mode 100644 index 0000000..a7eccc9 --- /dev/null +++ b/manuscripts/controller.py @@ -0,0 +1,148 @@ +from django.db.models import Q +from django.utils import timezone +from django.utils.dateparse import parse_date + +from ai.utils.normalizers import stz_text, stz_year +from journals.models import Issue, Journal + +from manuscripts.choices import EventStatus +from manuscripts.models.article import Article +from manuscripts.models.processing import ProcessingEvent +from manuscripts.utils.helpers import checksum_bytes, json_safe +from manuscripts.utils.xml_utils import extract_article_metadata + + +def get_or_create_article(processing, xml_content=None, title=""): + metadata = extract_article_metadata(xml_content) if xml_content else {} + checksum = checksum_bytes(xml_content) if xml_content else processing.input_checksum + article = None + if metadata.get("doi"): + article = Article.objects.filter(doi=metadata["doi"]).first() + if not article and metadata.get("pid"): + article = Article.objects.filter(pid=metadata["pid"]).first() + if not article: + article = Article.objects.filter(content_checksum=checksum).first() + + artdate_val = metadata.get("artdate") + if isinstance(artdate_val, str): + artdate_val = parse_date(artdate_val) + + if not article: + article = Article.objects.create( + title=metadata.get("title") or title or processing.title, + doi=metadata.get("doi", ""), + pid=metadata.get("pid", ""), + content_checksum=checksum, + creator=processing.creator, + language=metadata.get("language", "en"), + license=metadata.get("license"), + elocatid=metadata.get("elocatid", ""), + fpage=metadata.get("fpage", ""), + lpage=metadata.get("lpage", ""), + seq=metadata.get("seq", ""), + artdate=artdate_val, + ) + else: + changed = False + fields_to_update = { + "title": metadata.get("title"), + "doi": metadata.get("doi"), + "pid": metadata.get("pid"), + "language": metadata.get("language"), + "license": metadata.get("license"), + "elocatid": metadata.get("elocatid"), + "fpage": metadata.get("fpage"), + "lpage": metadata.get("lpage"), + "seq": metadata.get("seq"), + "artdate": artdate_val, + } + for field, value in fields_to_update.items(): + if value and not getattr(article, field): + setattr(article, field, value) + changed = True + if changed: + article.save() + processing.articles.add(article) + return article, metadata + + +def enrich_article(payload, article): + updates = {} + if payload.get("doi") and payload["doi"] != article.doi: + updates["doi"] = payload["doi"] + if payload.get("titles"): + title = payload["titles"][0]["text"] + if title and title != article.title: + updates["title"] = title + for item in payload.get("dates") or []: + parsed = parse_date(item.get("date") or "") + if not parsed: + continue + if item.get("type") == "published" and parsed != article.artdate: + updates["artdate"] = parsed + elif item.get("type") == "ahp" and parsed != article.ahpdate: + updates["ahpdate"] = parsed + + journal_payload = payload.get("journal") + matched_journal = article.journal + if journal_payload and not matched_journal: + j_title = journal_payload.get("title") + j_issn = journal_payload.get("issn") + journal_qs = Journal.objects.none() + if j_issn: + clean_issn = str(j_issn).replace("-", "").strip() + journal_qs = Journal.objects.filter( + Q(issn__icontains=clean_issn) | Q(pissn__icontains=clean_issn) | Q(eissn__icontains=clean_issn) + | Q(issn__icontains=str(j_issn).strip()) | Q(pissn__icontains=str(j_issn).strip()) | Q(eissn__icontains=str(j_issn).strip()) + ) + if not journal_qs.exists() and j_title: + journal_qs = Journal.objects.filter( + Q(title__iexact=j_title) | Q(short_title__iexact=j_title) | Q(title_nlm__iexact=j_title) + ) + matched_journal = journal_qs.first() + if matched_journal: + updates["journal"] = matched_journal + + if matched_journal and not article.issue: + issue_payload = payload.get("issue") + if issue_payload: + vol = stz_text(issue_payload.get("volume")) + num = stz_text(issue_payload.get("number")) + yr = stz_year(issue_payload.get("year")) + supp = stz_text(issue_payload.get("supplement")) + issue_qs = Issue.objects.filter(journal=matched_journal) + if yr: + issue_qs = issue_qs.filter(year=yr) + if vol: + issue_qs = issue_qs.filter(volume=vol) + if num: + issue_qs = issue_qs.filter(number=num) + if supp: + issue_qs = issue_qs.filter(supplement=supp) + matched_issue = issue_qs.first() + if matched_issue: + updates["issue"] = matched_issue + + return updates + + +def event_start(processing, action, task_id, article=None): + processing.current_action = action + processing.save(update_fields=["current_action", "updated"]) + return ProcessingEvent.objects.create( + processing=processing, article=article, action=action, status=EventStatus.RUNNING, + task_id=task_id or "", started_at=timezone.now(), + ) + + +def event_complete(event, message="", details=None, status=EventStatus.COMPLETED): + event.status = status + event.message = message + event.details = json_safe(details or {}) + event.completed_at = timezone.now() + event.save() + + +def event_details_update(event, details): + event.details = json_safe({**(event.details or {}), **(details or {})}) + event.save(update_fields=["details", "updated"]) diff --git a/manuscripts/forms.py b/manuscripts/forms.py new file mode 100644 index 0000000..08a1ef1 --- /dev/null +++ b/manuscripts/forms.py @@ -0,0 +1,95 @@ +import os +import zipfile + +from django import forms +from django.core.exceptions import ValidationError +from django.utils.translation import gettext_lazy as _ + +from core.forms import CoreAdminModelForm + +from .choices import InputType, ProcessingAction +from .models.article import ArticleStructureVersion +from .models.processing import Processing +from .utils.inspection import suggested_actions + + +class ProcessingUploadForm(CoreAdminModelForm): + class Meta: + model = Processing + fields = ["title", "input_file"] + + def clean_input_file(self): + uploaded = self.cleaned_data["input_file"] + extension = os.path.splitext(uploaded.name)[1].lower() + if extension not in {".docx", ".xml", ".zip"}: + raise ValidationError(_("Upload a DOCX document, SPS XML, or ZIP package.")) + if uploaded.size == 0: + raise ValidationError(_("The file is empty.")) + if uploaded.size > 250 * 1024 * 1024: + raise ValidationError(_("The file cannot exceed 250 MB.")) + if extension == ".zip" and not zipfile.is_zipfile(uploaded): + raise ValidationError(_("The ZIP file is invalid.")) + return uploaded + + +class XMLUploadForm(ProcessingUploadForm): + def clean_input_file(self): + uploaded = super().clean_input_file() + if os.path.splitext(uploaded.name)[1].lower() != ".xml": + raise ValidationError(_("Upload a SPS XML file.")) + return uploaded + + +class DOCXUploadForm(ProcessingUploadForm): + def clean_input_file(self): + uploaded = super().clean_input_file() + if os.path.splitext(uploaded.name)[1].lower() != ".docx": + raise ValidationError(_("Upload a DOCX file.")) + return uploaded + + +class SPSPackageUploadForm(ProcessingUploadForm): + def clean_input_file(self): + uploaded = super().clean_input_file() + if os.path.splitext(uploaded.name)[1].lower() != ".zip": + raise ValidationError(_("Upload a SPS ZIP package.")) + return uploaded + + +class ArticleStructureForm(CoreAdminModelForm): + class Meta: + model = ArticleStructureVersion + fields = ["front", "body", "back"] + + +class ProcessingReviewForm(forms.ModelForm): + requested_actions = forms.MultipleChoiceField( + label=_("Actions to execute"), + choices=ProcessingAction.choices, + widget=forms.CheckboxSelectMultiple, + required=False, + ) + + class Meta: + model = Processing + fields = ["requested_actions"] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + applicable = set(suggested_actions(self.instance.detected_type)) + if self.instance.detected_type == InputType.SPS_PACKAGE: + applicable.update({ProcessingAction.HTML_GENERATION, ProcessingAction.PDF_GENERATION}) + self.fields["requested_actions"].choices = [ + c for c in ProcessingAction.choices if c[0] in applicable + ] + + def clean(self): + cleaned = super().clean() + actions = cleaned.get("requested_actions") or [] + allowed = set(suggested_actions(self.instance.detected_type)) + if self.instance.detected_type == InputType.SPS_PACKAGE: + allowed.update({ProcessingAction.HTML_GENERATION, ProcessingAction.PDF_GENERATION}) + invalid = set(actions) - allowed + if invalid: + raise ValidationError(_("Some actions are incompatible with the input type.")) + return cleaned diff --git a/manuscripts/migrations/0001_initial.py b/manuscripts/migrations/0001_initial.py new file mode 100644 index 0000000..fbe8cb5 --- /dev/null +++ b/manuscripts/migrations/0001_initial.py @@ -0,0 +1,168 @@ +# Generated by Django 6.0.5 on 2026-06-09 00:35 + +import wagtail.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Article', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Last update date')), + ('title', models.TextField(blank=True, verbose_name='Provisional title')), + ('doi', models.CharField(blank=True, db_index=True, max_length=255, verbose_name='DOI')), + ('pid', models.CharField(blank=True, db_index=True, max_length=255, verbose_name='PID')), + ('content_checksum', models.CharField(blank=True, db_index=True, max_length=64, verbose_name='Content checksum')), + ('status', models.CharField(choices=[('awaiting_review', 'Awaiting review'), ('pending', 'Pending'), ('processing', 'Processing'), ('completed', 'Completed'), ('failed', 'Failed'), ('partial', 'Partial'), ('cancelled', 'Cancelled')], default='pending', max_length=20, verbose_name='Status')), + ('language', models.CharField(blank=True, choices=[('aa', 'Afar'), ('af', 'Afrikaans'), ('ak', 'Akan'), ('sq', 'Albanian'), ('am', 'Amharic'), ('ar', 'Arabic'), ('an', 'Aragonese'), ('hy', 'Armenian'), ('as', 'Assamese'), ('av', 'Avaric'), ('ae', 'Avestan'), ('ay', 'Aymara'), ('az', 'Azerbaijani'), ('bm', 'Bambara'), ('ba', 'Bashkir'), ('eu', 'Basque'), ('be', 'Belarusian'), ('bn', 'Bengali'), ('bi', 'Bislama'), ('bs', 'Bosnian'), ('br', 'Breton'), ('bg', 'Bulgarian'), ('my', 'Burmese'), ('ca', 'Catalan, Valencian'), ('ch', 'Chamorro'), ('ce', 'Chechen'), ('ny', 'Chichewa, Chewa, Nyanja'), ('zh', 'Chinese'), ('cu', 'Church Slavic, Old Slavonic, Church Slavonic, Old Bulgarian, Old Church Slavonic'), ('cv', 'Chuvash'), ('kw', 'Cornish'), ('co', 'Corsican'), ('cr', 'Cree'), ('hr', 'Croatian'), ('cs', 'Czech'), ('da', 'Danish'), ('dv', 'Divehi, Dhivehi, Maldivian'), ('nl', 'Dutch, Flemish'), ('dz', 'Dzongkha'), ('en', 'English'), ('eo', 'Esperanto'), ('et', 'Estonian'), ('ee', 'Ewe'), ('fo', 'Faroese'), ('fj', 'Fijian'), ('fi', 'Finnish'), ('fr', 'French'), ('fy', 'Western Frisian'), ('ff', 'Fulah'), ('gd', 'Gaelic, Scottish Gaelic'), ('gl', 'Galician'), ('lg', 'Ganda'), ('ka', 'Georgian'), ('de', 'German'), ('el', 'Greek, Modern (1453–)'), ('kl', 'Kalaallisut, Greenlandic'), ('gn', 'Guarani'), ('gu', 'Gujarati'), ('ht', 'Haitian, Haitian Creole'), ('ha', 'Hausa'), ('he', 'Hebrew'), ('hz', 'Herero'), ('hi', 'Hindi'), ('ho', 'Hiri Motu'), ('hu', 'Hungarian'), ('is', 'Icelandic'), ('io', 'Ido'), ('ig', 'Igbo'), ('id', 'Indonesian'), ('ia', 'Interlingua (International Auxiliary Language Association)'), ('ie', 'Interlingue, Occidental'), ('iu', 'Inuktitut'), ('ik', 'Inupiaq'), ('ga', 'Irish'), ('it', 'Italian'), ('ja', 'Japanese'), ('jv', 'Javanese'), ('kn', 'Kannada'), ('kr', 'Kanuri'), ('ks', 'Kashmiri'), ('kk', 'Kazakh'), ('km', 'Central Khmer'), ('ki', 'Kikuyu, Gikuyu'), ('rw', 'Kinyarwanda'), ('ky', 'Kirghiz, Kyrgyz'), ('kv', 'Komi'), ('kg', 'Kongo'), ('ko', 'Korean'), ('kj', 'Kuanyama, Kwanyama'), ('ku', 'Kurdish'), ('lo', 'Lao'), ('la', 'Latin'), ('lv', 'Latvian'), ('li', 'Limburgan, Limburger, Limburgish'), ('ln', 'Lingala'), ('lt', 'Lithuanian'), ('lu', 'Luba-Katanga'), ('lb', 'Luxembourgish, Letzeburgesch'), ('mk', 'Macedonian'), ('mg', 'Malagasy'), ('ms', 'Malay'), ('ml', 'Malayalam'), ('mt', 'Maltese'), ('gv', 'Manx'), ('mi', 'Maori'), ('mr', 'Marathi'), ('mh', 'Marshallese'), ('mn', 'Mongolian'), ('na', 'Nauru'), ('nv', 'Navajo, Navaho'), ('nd', 'North Ndebele'), ('nr', 'South Ndebele'), ('ng', 'Ndonga'), ('ne', 'Nepali'), ('no', 'Norwegian'), ('nb', 'Norwegian Bokmål'), ('nn', 'Norwegian Nynorsk'), ('ii', 'Sichuan Yi, Nuosu'), ('oc', 'Occitan'), ('oj', 'Ojibwa'), ('or', 'Oriya'), ('om', 'Oromo'), ('os', 'Ossetian, Ossetic'), ('pi', 'Pali'), ('ps', 'Pashto, Pushto'), ('fa', 'Persian'), ('pl', 'Polish'), ('pt', 'Português'), ('pa', 'Punjabi, Panjabi'), ('qu', 'Quechua'), ('ro', 'Romanian, Moldavian, Moldovan'), ('rm', 'Romansh'), ('rn', 'Rundi'), ('ru', 'Russian'), ('se', 'Northern Sami'), ('sm', 'Samoan'), ('sg', 'Sango'), ('sa', 'Sanskrit'), ('sc', 'Sardinian'), ('sr', 'Serbian'), ('sn', 'Shona'), ('sd', 'Sindhi'), ('si', 'Sinhala, Sinhalese'), ('sk', 'Slovak'), ('sl', 'Slovenian'), ('so', 'Somali'), ('st', 'Southern Sotho'), ('es', 'Español'), ('su', 'Sundanese'), ('sw', 'Swahili'), ('ss', 'Swati'), ('sv', 'Swedish'), ('tl', 'Tagalog'), ('ty', 'Tahitian'), ('tg', 'Tajik'), ('ta', 'Tamil'), ('tt', 'Tatar'), ('te', 'Telugu'), ('th', 'Thai'), ('bo', 'Tibetan'), ('ti', 'Tigrinya'), ('to', 'Tonga (Tonga Islands)'), ('ts', 'Tsonga'), ('tn', 'Tswana'), ('tr', 'Turkish'), ('tk', 'Turkmen'), ('tw', 'Twi'), ('ug', 'Uighur, Uyghur'), ('uk', 'Ukrainian'), ('ur', 'Urdu'), ('uz', 'Uzbek'), ('ve', 'Venda'), ('vi', 'Vietnamese'), ('vo', 'Volapük'), ('wa', 'Walloon'), ('cy', 'Welsh'), ('wo', 'Wolof'), ('xh', 'Xhosa'), ('yi', 'Yiddish'), ('yo', 'Yoruba'), ('za', 'Zhuang, Chuang'), ('zu', 'Zulu')], default='en', max_length=10, verbose_name='Language')), + ('license', models.URLField(blank=True, max_length=500, null=True, verbose_name='License (URL)')), + ('fpage', models.CharField(blank=True, max_length=32, verbose_name='First page')), + ('lpage', models.CharField(blank=True, max_length=32, verbose_name='Last page')), + ('seq', models.CharField(blank=True, max_length=32, verbose_name='Sequence')), + ('elocatid', models.CharField(blank=True, max_length=255, verbose_name='Elocation ID')), + ('artdate', models.DateField(blank=True, null=True, verbose_name='Publication date')), + ('ahpdate', models.DateField(blank=True, null=True, verbose_name='AHP date')), + ('metadata', models.JSONField(blank=True, default=dict, verbose_name='Metadata')), + ], + options={ + 'verbose_name': 'Article', + 'verbose_name_plural': 'Articles', + 'ordering': ['-created'], + }, + ), + migrations.CreateModel( + name='ArticleArtifact', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Last update date')), + ('artifact_type', models.CharField(choices=[('input', 'Uploaded file'), ('source_document', 'Original document'), ('marked_document', 'Document with marked citations'), ('structure', 'XML structure'), ('xml', 'XML SPS'), ('asset', 'Asset'), ('sps_package', 'SPS Package'), ('validation_report', 'Validation report'), ('validation_exceptions', 'Validation exceptions'), ('html', 'HTML'), ('pdf', 'PDF'), ('intermediate_document', 'Intermediate document')], max_length=32, verbose_name='Type')), + ('file', models.FileField(upload_to='processings/artifacts/%Y/%m/%d/', verbose_name='File')), + ('original_path', models.CharField(blank=True, max_length=1024, verbose_name='Original path')), + ('source_url', models.URLField(blank=True, max_length=2048, verbose_name='Source URL')), + ('version', models.PositiveIntegerField(default=1, verbose_name='Version')), + ('is_current', models.BooleanField(default=True, verbose_name='Current version')), + ('file_size_bytes', models.PositiveBigIntegerField(default=0, verbose_name='Size')), + ('checksum', models.CharField(blank=True, db_index=True, max_length=64, verbose_name='SHA256')), + ('metadata', models.JSONField(blank=True, default=dict, verbose_name='Metadata')), + ('is_stale', models.BooleanField(default=False, verbose_name='Outdated')), + ], + options={ + 'verbose_name': 'Artifact', + 'verbose_name_plural': 'Artifacts', + 'ordering': ['processing', 'article', 'artifact_type', '-version'], + }, + ), + migrations.CreateModel( + name='ArticleReference', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Last update date')), + ('position', models.PositiveIntegerField(verbose_name='Order')), + ('ref_id', models.CharField(max_length=32, verbose_name='SPS ID')), + ('mixed_citation', models.TextField(verbose_name='Original reference')), + ('metadata', models.JSONField(blank=True, default=dict, verbose_name='Structured data')), + ], + options={ + 'verbose_name': 'Article reference', + 'verbose_name_plural': 'Article references', + 'ordering': ['structure', 'position'], + }, + ), + migrations.CreateModel( + name='ArticleStructureVersion', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Last update date')), + ('version', models.PositiveIntegerField(default=1, verbose_name='Version')), + ('is_current', models.BooleanField(default=True, verbose_name='Current version')), + ('source_kind', models.CharField(choices=[('unknown', 'Unidentified'), ('document', 'Document'), ('xml', 'XML SPS'), ('sps_package', 'SPS Package'), ('source_package', 'Source package'), ('ambiguous_zip', 'Ambiguous ZIP package')], default='document', max_length=32, verbose_name='Origin')), + ('front', wagtail.fields.StreamField([('paragraph_with_language', 3), ('paragraph', 5), ('author_paragraph', 11), ('aff_paragraph', 22)], blank=True, block_lookup={0: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('<abstract>', '<abstract>'), ('<abstract-title>', '<abstract-title>'), ('<aff>', '<aff>'), ('<article-id>', '<article-id>'), ('<article-title>', '<article-title>'), ('<author-notes>', '<author-notes>'), ('<contrib>', '<contrib>'), ('<date-accepted>', '<date-accepted>'), ('<date-received>', '<date-received>'), ('<fig>', '<fig>'), ('<fig-attrib>', '<fig-attrib>'), ('<history>', '<history>'), ('<kwd-title>', '<kwd-title>'), ('<kwd-group>', '<kwd-group>'), ('<list>', '<list>'), ('<p>', '<p>'), ('<sec>', '<sec>'), ('<sub-sec>', '<sub-sec>'), ('<subject>', '<subject>'), ('<table>', '<table>'), ('<table-foot>', '<table-foot>'), ('<title>', '<title>'), ('<trans-abstract>', '<trans-abstract>'), ('<trans-title>', '<trans-title>'), ('<translate-front>', '<translate-front>'), ('<translate-body>', '<translate-body>'), ('<disp-formula>', '<disp-formula>'), ('<inline-formula>', '<inline-formula>'), ('<formula>', '<formula>')], 'label': 'Label', 'required': False}), 1: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('aa', 'Afar'), ('af', 'Afrikaans'), ('ak', 'Akan'), ('sq', 'Albanian'), ('am', 'Amharic'), ('ar', 'Arabic'), ('an', 'Aragonese'), ('hy', 'Armenian'), ('as', 'Assamese'), ('av', 'Avaric'), ('ae', 'Avestan'), ('ay', 'Aymara'), ('az', 'Azerbaijani'), ('bm', 'Bambara'), ('ba', 'Bashkir'), ('eu', 'Basque'), ('be', 'Belarusian'), ('bn', 'Bengali'), ('bi', 'Bislama'), ('bs', 'Bosnian'), ('br', 'Breton'), ('bg', 'Bulgarian'), ('my', 'Burmese'), ('ca', 'Catalan, Valencian'), ('ch', 'Chamorro'), ('ce', 'Chechen'), ('ny', 'Chichewa, Chewa, Nyanja'), ('zh', 'Chinese'), ('cu', 'Church Slavic, Old Slavonic, Church Slavonic, Old Bulgarian, Old Church Slavonic'), ('cv', 'Chuvash'), ('kw', 'Cornish'), ('co', 'Corsican'), ('cr', 'Cree'), ('hr', 'Croatian'), ('cs', 'Czech'), ('da', 'Danish'), ('dv', 'Divehi, Dhivehi, Maldivian'), ('nl', 'Dutch, Flemish'), ('dz', 'Dzongkha'), ('en', 'English'), ('eo', 'Esperanto'), ('et', 'Estonian'), ('ee', 'Ewe'), ('fo', 'Faroese'), ('fj', 'Fijian'), ('fi', 'Finnish'), ('fr', 'French'), ('fy', 'Western Frisian'), ('ff', 'Fulah'), ('gd', 'Gaelic, Scottish Gaelic'), ('gl', 'Galician'), ('lg', 'Ganda'), ('ka', 'Georgian'), ('de', 'German'), ('el', 'Greek, Modern (1453–)'), ('kl', 'Kalaallisut, Greenlandic'), ('gn', 'Guarani'), ('gu', 'Gujarati'), ('ht', 'Haitian, Haitian Creole'), ('ha', 'Hausa'), ('he', 'Hebrew'), ('hz', 'Herero'), ('hi', 'Hindi'), ('ho', 'Hiri Motu'), ('hu', 'Hungarian'), ('is', 'Icelandic'), ('io', 'Ido'), ('ig', 'Igbo'), ('id', 'Indonesian'), ('ia', 'Interlingua (International Auxiliary Language Association)'), ('ie', 'Interlingue, Occidental'), ('iu', 'Inuktitut'), ('ik', 'Inupiaq'), ('ga', 'Irish'), ('it', 'Italian'), ('ja', 'Japanese'), ('jv', 'Javanese'), ('kn', 'Kannada'), ('kr', 'Kanuri'), ('ks', 'Kashmiri'), ('kk', 'Kazakh'), ('km', 'Central Khmer'), ('ki', 'Kikuyu, Gikuyu'), ('rw', 'Kinyarwanda'), ('ky', 'Kirghiz, Kyrgyz'), ('kv', 'Komi'), ('kg', 'Kongo'), ('ko', 'Korean'), ('kj', 'Kuanyama, Kwanyama'), ('ku', 'Kurdish'), ('lo', 'Lao'), ('la', 'Latin'), ('lv', 'Latvian'), ('li', 'Limburgan, Limburger, Limburgish'), ('ln', 'Lingala'), ('lt', 'Lithuanian'), ('lu', 'Luba-Katanga'), ('lb', 'Luxembourgish, Letzeburgesch'), ('mk', 'Macedonian'), ('mg', 'Malagasy'), ('ms', 'Malay'), ('ml', 'Malayalam'), ('mt', 'Maltese'), ('gv', 'Manx'), ('mi', 'Maori'), ('mr', 'Marathi'), ('mh', 'Marshallese'), ('mn', 'Mongolian'), ('na', 'Nauru'), ('nv', 'Navajo, Navaho'), ('nd', 'North Ndebele'), ('nr', 'South Ndebele'), ('ng', 'Ndonga'), ('ne', 'Nepali'), ('no', 'Norwegian'), ('nb', 'Norwegian Bokmål'), ('nn', 'Norwegian Nynorsk'), ('ii', 'Sichuan Yi, Nuosu'), ('oc', 'Occitan'), ('oj', 'Ojibwa'), ('or', 'Oriya'), ('om', 'Oromo'), ('os', 'Ossetian, Ossetic'), ('pi', 'Pali'), ('ps', 'Pashto, Pushto'), ('fa', 'Persian'), ('pl', 'Polish'), ('pt', 'Português'), ('pa', 'Punjabi, Panjabi'), ('qu', 'Quechua'), ('ro', 'Romanian, Moldavian, Moldovan'), ('rm', 'Romansh'), ('rn', 'Rundi'), ('ru', 'Russian'), ('se', 'Northern Sami'), ('sm', 'Samoan'), ('sg', 'Sango'), ('sa', 'Sanskrit'), ('sc', 'Sardinian'), ('sr', 'Serbian'), ('sn', 'Shona'), ('sd', 'Sindhi'), ('si', 'Sinhala, Sinhalese'), ('sk', 'Slovak'), ('sl', 'Slovenian'), ('so', 'Somali'), ('st', 'Southern Sotho'), ('es', 'Español'), ('su', 'Sundanese'), ('sw', 'Swahili'), ('ss', 'Swati'), ('sv', 'Swedish'), ('tl', 'Tagalog'), ('ty', 'Tahitian'), ('tg', 'Tajik'), ('ta', 'Tamil'), ('tt', 'Tatar'), ('te', 'Telugu'), ('th', 'Thai'), ('bo', 'Tibetan'), ('ti', 'Tigrinya'), ('to', 'Tonga (Tonga Islands)'), ('ts', 'Tsonga'), ('tn', 'Tswana'), ('tr', 'Turkish'), ('tk', 'Turkmen'), ('tw', 'Twi'), ('ug', 'Uighur, Uyghur'), ('uk', 'Ukrainian'), ('ur', 'Urdu'), ('uz', 'Uzbek'), ('ve', 'Venda'), ('vi', 'Vietnamese'), ('vo', 'Volapük'), ('wa', 'Walloon'), ('cy', 'Welsh'), ('wo', 'Wolof'), ('xh', 'Xhosa'), ('yi', 'Yiddish'), ('yo', 'Yoruba'), ('za', 'Zhuang, Chuang'), ('zu', 'Zulu')], 'label': 'Language', 'required': False}), 2: ('wagtail.blocks.TextBlock', (), {'label': 'Title', 'required': False}), 3: ('wagtail.blocks.StructBlock', [[('label', 0), ('language', 1), ('paragraph', 2)]], {}), 4: ('wagtail.blocks.TextBlock', (), {'label': 'Paragraph', 'required': False}), 5: ('wagtail.blocks.StructBlock', [[('label', 0), ('paragraph', 4)]], {}), 6: ('wagtail.blocks.TextBlock', (), {'label': 'Surname', 'required': False}), 7: ('wagtail.blocks.TextBlock', (), {'label': 'Given names', 'required': False}), 8: ('wagtail.blocks.TextBlock', (), {'label': 'Orcid', 'required': False}), 9: ('wagtail.blocks.TextBlock', (), {'label': 'Aff id', 'required': False}), 10: ('wagtail.blocks.TextBlock', (), {'label': 'Char link', 'required': False}), 11: ('wagtail.blocks.StructBlock', [[('label', 0), ('paragraph', 4), ('surname', 6), ('given_names', 7), ('orcid', 8), ('affid', 9), ('char', 10)]], {}), 12: ('wagtail.blocks.TextBlock', (), {'label': 'Full text Aff', 'required': False}), 13: ('wagtail.blocks.TextBlock', (), {'label': 'Orgname', 'required': False}), 14: ('wagtail.blocks.TextBlock', (), {'label': 'Orgdiv2', 'required': False}), 15: ('wagtail.blocks.TextBlock', (), {'label': 'Orgdiv1', 'required': False}), 16: ('wagtail.blocks.TextBlock', (), {'label': 'Zipcode', 'required': False}), 17: ('wagtail.blocks.TextBlock', (), {'label': 'City', 'required': False}), 18: ('wagtail.blocks.TextBlock', (), {'label': 'State', 'required': False}), 19: ('wagtail.blocks.TextBlock', (), {'label': 'Country', 'required': False}), 20: ('wagtail.blocks.TextBlock', (), {'label': 'Code country', 'required': False}), 21: ('wagtail.blocks.TextBlock', (), {'label': 'Original', 'required': False}), 22: ('wagtail.blocks.StructBlock', [[('label', 0), ('paragraph', 4), ('affid', 9), ('text_aff', 12), ('char', 10), ('orgname', 13), ('orgdiv2', 14), ('orgdiv1', 15), ('zipcode', 16), ('city', 17), ('state', 18), ('country', 19), ('code_country', 20), ('original', 21)]], {})})), + ('body', wagtail.fields.StreamField([('paragraph', 2), ('paragraph_with_language', 5), ('compound_paragraph', 10), ('image', 15), ('table', 19)], blank=True, block_lookup={0: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('<abstract>', '<abstract>'), ('<abstract-title>', '<abstract-title>'), ('<aff>', '<aff>'), ('<article-id>', '<article-id>'), ('<article-title>', '<article-title>'), ('<author-notes>', '<author-notes>'), ('<contrib>', '<contrib>'), ('<date-accepted>', '<date-accepted>'), ('<date-received>', '<date-received>'), ('<fig>', '<fig>'), ('<fig-attrib>', '<fig-attrib>'), ('<history>', '<history>'), ('<kwd-title>', '<kwd-title>'), ('<kwd-group>', '<kwd-group>'), ('<list>', '<list>'), ('<p>', '<p>'), ('<sec>', '<sec>'), ('<sub-sec>', '<sub-sec>'), ('<subject>', '<subject>'), ('<table>', '<table>'), ('<table-foot>', '<table-foot>'), ('<title>', '<title>'), ('<trans-abstract>', '<trans-abstract>'), ('<trans-title>', '<trans-title>'), ('<translate-front>', '<translate-front>'), ('<translate-body>', '<translate-body>'), ('<disp-formula>', '<disp-formula>'), ('<inline-formula>', '<inline-formula>'), ('<formula>', '<formula>')], 'label': 'Label', 'required': False}), 1: ('wagtail.blocks.TextBlock', (), {'label': 'Paragraph', 'required': False}), 2: ('wagtail.blocks.StructBlock', [[('label', 0), ('paragraph', 1)]], {}), 3: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('aa', 'Afar'), ('af', 'Afrikaans'), ('ak', 'Akan'), ('sq', 'Albanian'), ('am', 'Amharic'), ('ar', 'Arabic'), ('an', 'Aragonese'), ('hy', 'Armenian'), ('as', 'Assamese'), ('av', 'Avaric'), ('ae', 'Avestan'), ('ay', 'Aymara'), ('az', 'Azerbaijani'), ('bm', 'Bambara'), ('ba', 'Bashkir'), ('eu', 'Basque'), ('be', 'Belarusian'), ('bn', 'Bengali'), ('bi', 'Bislama'), ('bs', 'Bosnian'), ('br', 'Breton'), ('bg', 'Bulgarian'), ('my', 'Burmese'), ('ca', 'Catalan, Valencian'), ('ch', 'Chamorro'), ('ce', 'Chechen'), ('ny', 'Chichewa, Chewa, Nyanja'), ('zh', 'Chinese'), ('cu', 'Church Slavic, Old Slavonic, Church Slavonic, Old Bulgarian, Old Church Slavonic'), ('cv', 'Chuvash'), ('kw', 'Cornish'), ('co', 'Corsican'), ('cr', 'Cree'), ('hr', 'Croatian'), ('cs', 'Czech'), ('da', 'Danish'), ('dv', 'Divehi, Dhivehi, Maldivian'), ('nl', 'Dutch, Flemish'), ('dz', 'Dzongkha'), ('en', 'English'), ('eo', 'Esperanto'), ('et', 'Estonian'), ('ee', 'Ewe'), ('fo', 'Faroese'), ('fj', 'Fijian'), ('fi', 'Finnish'), ('fr', 'French'), ('fy', 'Western Frisian'), ('ff', 'Fulah'), ('gd', 'Gaelic, Scottish Gaelic'), ('gl', 'Galician'), ('lg', 'Ganda'), ('ka', 'Georgian'), ('de', 'German'), ('el', 'Greek, Modern (1453–)'), ('kl', 'Kalaallisut, Greenlandic'), ('gn', 'Guarani'), ('gu', 'Gujarati'), ('ht', 'Haitian, Haitian Creole'), ('ha', 'Hausa'), ('he', 'Hebrew'), ('hz', 'Herero'), ('hi', 'Hindi'), ('ho', 'Hiri Motu'), ('hu', 'Hungarian'), ('is', 'Icelandic'), ('io', 'Ido'), ('ig', 'Igbo'), ('id', 'Indonesian'), ('ia', 'Interlingua (International Auxiliary Language Association)'), ('ie', 'Interlingue, Occidental'), ('iu', 'Inuktitut'), ('ik', 'Inupiaq'), ('ga', 'Irish'), ('it', 'Italian'), ('ja', 'Japanese'), ('jv', 'Javanese'), ('kn', 'Kannada'), ('kr', 'Kanuri'), ('ks', 'Kashmiri'), ('kk', 'Kazakh'), ('km', 'Central Khmer'), ('ki', 'Kikuyu, Gikuyu'), ('rw', 'Kinyarwanda'), ('ky', 'Kirghiz, Kyrgyz'), ('kv', 'Komi'), ('kg', 'Kongo'), ('ko', 'Korean'), ('kj', 'Kuanyama, Kwanyama'), ('ku', 'Kurdish'), ('lo', 'Lao'), ('la', 'Latin'), ('lv', 'Latvian'), ('li', 'Limburgan, Limburger, Limburgish'), ('ln', 'Lingala'), ('lt', 'Lithuanian'), ('lu', 'Luba-Katanga'), ('lb', 'Luxembourgish, Letzeburgesch'), ('mk', 'Macedonian'), ('mg', 'Malagasy'), ('ms', 'Malay'), ('ml', 'Malayalam'), ('mt', 'Maltese'), ('gv', 'Manx'), ('mi', 'Maori'), ('mr', 'Marathi'), ('mh', 'Marshallese'), ('mn', 'Mongolian'), ('na', 'Nauru'), ('nv', 'Navajo, Navaho'), ('nd', 'North Ndebele'), ('nr', 'South Ndebele'), ('ng', 'Ndonga'), ('ne', 'Nepali'), ('no', 'Norwegian'), ('nb', 'Norwegian Bokmål'), ('nn', 'Norwegian Nynorsk'), ('ii', 'Sichuan Yi, Nuosu'), ('oc', 'Occitan'), ('oj', 'Ojibwa'), ('or', 'Oriya'), ('om', 'Oromo'), ('os', 'Ossetian, Ossetic'), ('pi', 'Pali'), ('ps', 'Pashto, Pushto'), ('fa', 'Persian'), ('pl', 'Polish'), ('pt', 'Português'), ('pa', 'Punjabi, Panjabi'), ('qu', 'Quechua'), ('ro', 'Romanian, Moldavian, Moldovan'), ('rm', 'Romansh'), ('rn', 'Rundi'), ('ru', 'Russian'), ('se', 'Northern Sami'), ('sm', 'Samoan'), ('sg', 'Sango'), ('sa', 'Sanskrit'), ('sc', 'Sardinian'), ('sr', 'Serbian'), ('sn', 'Shona'), ('sd', 'Sindhi'), ('si', 'Sinhala, Sinhalese'), ('sk', 'Slovak'), ('sl', 'Slovenian'), ('so', 'Somali'), ('st', 'Southern Sotho'), ('es', 'Español'), ('su', 'Sundanese'), ('sw', 'Swahili'), ('ss', 'Swati'), ('sv', 'Swedish'), ('tl', 'Tagalog'), ('ty', 'Tahitian'), ('tg', 'Tajik'), ('ta', 'Tamil'), ('tt', 'Tatar'), ('te', 'Telugu'), ('th', 'Thai'), ('bo', 'Tibetan'), ('ti', 'Tigrinya'), ('to', 'Tonga (Tonga Islands)'), ('ts', 'Tsonga'), ('tn', 'Tswana'), ('tr', 'Turkish'), ('tk', 'Turkmen'), ('tw', 'Twi'), ('ug', 'Uighur, Uyghur'), ('uk', 'Ukrainian'), ('ur', 'Urdu'), ('uz', 'Uzbek'), ('ve', 'Venda'), ('vi', 'Vietnamese'), ('vo', 'Volapük'), ('wa', 'Walloon'), ('cy', 'Welsh'), ('wo', 'Wolof'), ('xh', 'Xhosa'), ('yi', 'Yiddish'), ('yo', 'Yoruba'), ('za', 'Zhuang, Chuang'), ('zu', 'Zulu')], 'label': 'Language', 'required': False}), 4: ('wagtail.blocks.TextBlock', (), {'label': 'Title', 'required': False}), 5: ('wagtail.blocks.StructBlock', [[('label', 0), ('language', 3), ('paragraph', 4)]], {}), 6: ('wagtail.blocks.TextBlock', (), {'label': 'Equation id', 'required': False}), 7: ('wagtail.blocks.TextBlock', (), {'label': 'Text'}), 8: ('wagtail.blocks.TextBlock', (), {'label': 'Formula'}), 9: ('wagtail.blocks.StreamBlock', [[('text', 7), ('formula', 8)]], {'label': 'Content', 'required': True}), 10: ('wagtail.blocks.StructBlock', [[('label', 0), ('eid', 6), ('content', 9)]], {}), 11: ('wagtail.blocks.TextBlock', (), {'label': 'Fig id', 'required': False}), 12: ('wagtail.blocks.TextBlock', (), {'label': 'Fig label', 'required': False}), 13: ('wagtail.blocks.TextBlock', (), {'label': 'Alt text', 'required': False}), 14: ('wagtail.images.blocks.ImageChooserBlock', (), {'required': True}), 15: ('wagtail.blocks.StructBlock', [[('label', 0), ('figid', 11), ('figlabel', 12), ('title', 4), ('alttext', 13), ('image', 14)]], {}), 16: ('wagtail.blocks.TextBlock', (), {'label': 'Table id', 'required': False}), 17: ('wagtail.blocks.TextBlock', (), {'label': 'Table label', 'required': False}), 18: ('wagtail.blocks.TextBlock', (), {'label': 'Content', 'required': False}), 19: ('wagtail.blocks.StructBlock', [[('label', 0), ('tabid', 16), ('tablabel', 17), ('title', 4), ('content', 18)]], {})})), + ('back', wagtail.fields.StreamField([('paragraph', 2), ('ref_paragraph', 29)], blank=True, block_lookup={0: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('<abstract>', '<abstract>'), ('<abstract-title>', '<abstract-title>'), ('<aff>', '<aff>'), ('<article-id>', '<article-id>'), ('<article-title>', '<article-title>'), ('<author-notes>', '<author-notes>'), ('<contrib>', '<contrib>'), ('<date-accepted>', '<date-accepted>'), ('<date-received>', '<date-received>'), ('<fig>', '<fig>'), ('<fig-attrib>', '<fig-attrib>'), ('<history>', '<history>'), ('<kwd-title>', '<kwd-title>'), ('<kwd-group>', '<kwd-group>'), ('<list>', '<list>'), ('<p>', '<p>'), ('<sec>', '<sec>'), ('<sub-sec>', '<sub-sec>'), ('<subject>', '<subject>'), ('<table>', '<table>'), ('<table-foot>', '<table-foot>'), ('<title>', '<title>'), ('<trans-abstract>', '<trans-abstract>'), ('<trans-title>', '<trans-title>'), ('<translate-front>', '<translate-front>'), ('<translate-body>', '<translate-body>'), ('<disp-formula>', '<disp-formula>'), ('<inline-formula>', '<inline-formula>'), ('<formula>', '<formula>')], 'label': 'Label', 'required': False}), 1: ('wagtail.blocks.TextBlock', (), {'label': 'Paragraph', 'required': False}), 2: ('wagtail.blocks.StructBlock', [[('label', 0), ('paragraph', 1)]], {}), 3: ('wagtail.blocks.TextBlock', (), {'label': 'Ref type', 'required': False}), 4: ('wagtail.blocks.TextBlock', (), {'label': 'Ref id', 'required': False}), 5: ('wagtail.blocks.TextBlock', (), {'label': 'Surname', 'required': False}), 6: ('wagtail.blocks.TextBlock', (), {'label': 'Given names', 'required': False}), 7: ('wagtail.blocks.StructBlock', [[('surname', 5), ('given_names', 6)]], {}), 8: ('wagtail.blocks.StreamBlock', [[('Author', 7)]], {'label': 'Authors', 'required': False}), 9: ('wagtail.blocks.TextBlock', (), {'label': 'Date', 'required': False}), 10: ('wagtail.blocks.TextBlock', (), {'label': 'Title', 'required': False}), 11: ('wagtail.blocks.TextBlock', (), {'label': 'Chapter', 'required': False}), 12: ('wagtail.blocks.TextBlock', (), {'label': 'Edition', 'required': False}), 13: ('wagtail.blocks.TextBlock', (), {'label': 'Source', 'required': False}), 14: ('wagtail.blocks.TextBlock', (), {'label': 'Vol', 'required': False}), 15: ('wagtail.blocks.TextBlock', (), {'label': 'Issue', 'required': False}), 16: ('wagtail.blocks.TextBlock', (), {'label': 'Pages', 'required': False}), 17: ('wagtail.blocks.TextBlock', (), {'label': 'First page', 'required': False}), 18: ('wagtail.blocks.TextBlock', (), {'label': 'Last page', 'required': False}), 19: ('wagtail.blocks.TextBlock', (), {'label': 'DOI', 'required': False}), 20: ('wagtail.blocks.TextBlock', (), {'label': 'Access id', 'required': False}), 21: ('wagtail.blocks.TextBlock', (), {'label': 'Degree', 'required': False}), 22: ('wagtail.blocks.TextBlock', (), {'label': 'Organization', 'required': False}), 23: ('wagtail.blocks.TextBlock', (), {'label': 'Location', 'required': False}), 24: ('wagtail.blocks.TextBlock', (), {'label': 'Org location', 'required': False}), 25: ('wagtail.blocks.TextBlock', (), {'label': 'Num pages', 'required': False}), 26: ('wagtail.blocks.TextBlock', (), {'label': 'Uri', 'required': False}), 27: ('wagtail.blocks.TextBlock', (), {'label': 'Version', 'required': False}), 28: ('wagtail.blocks.TextBlock', (), {'label': 'Access date', 'required': False}), 29: ('wagtail.blocks.StructBlock', [[('label', 0), ('paragraph', 1), ('reftype', 3), ('refid', 4), ('authors', 8), ('date', 9), ('title', 10), ('chapter', 11), ('edition', 12), ('source', 13), ('vol', 14), ('issue', 15), ('pages', 16), ('fpage', 17), ('lpage', 18), ('doi', 19), ('access_id', 20), ('degree', 21), ('organization', 22), ('location', 23), ('org_location', 24), ('num_pages', 25), ('uri', 26), ('version', 27), ('access_date', 28)]], {})})), + ('base_xml', models.TextField(blank=True, verbose_name='Preserved base XML')), + ('roundtrip_warnings', models.JSONField(blank=True, default=list, verbose_name='Round-trip warnings')), + ('xref_status', models.JSONField(blank=True, default=dict, verbose_name='Citation status')), + ], + options={ + 'verbose_name': 'Article structural version', + 'verbose_name_plural': 'Article structural versions', + 'ordering': ['article', '-version'], + }, + ), + migrations.CreateModel( + name='CitationOccurrence', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Last update date')), + ('text', models.TextField(verbose_name='In-text citation')), + ('location', models.JSONField(blank=True, default=dict, verbose_name='Location')), + ('status', models.CharField(choices=[('linked', 'Linked'), ('orphan', 'Orphan'), ('ambiguous', 'Ambiguous'), ('manual', 'Manually corrected')], max_length=16, verbose_name='Status')), + ], + options={ + 'verbose_name': 'Citation occurrence', + 'verbose_name_plural': 'Citation occurrences', + 'ordering': ['structure', 'created'], + }, + ), + migrations.CreateModel( + name='Processing', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Last update date')), + ('title', models.CharField(blank=True, max_length=255, verbose_name='Identification')), + ('input_file', models.FileField(upload_to='processings/input/%Y/%m/%d/', verbose_name='File to be uploaded')), + ('input_checksum', models.CharField(blank=True, db_index=True, max_length=64, verbose_name='SHA256')), + ('detected_type', models.CharField(choices=[('unknown', 'Unidentified'), ('document', 'Document'), ('xml', 'XML SPS'), ('sps_package', 'SPS Package'), ('source_package', 'Source package'), ('ambiguous_zip', 'Ambiguous ZIP package')], default='unknown', max_length=32, verbose_name='Detected type')), + ('confirmed_type', models.CharField(choices=[('unknown', 'Unidentified'), ('document', 'Document'), ('xml', 'XML SPS'), ('sps_package', 'SPS Package'), ('source_package', 'Source package'), ('ambiguous_zip', 'Ambiguous ZIP package')], default='unknown', max_length=32, verbose_name='Confirmed type')), + ('inspection', models.JSONField(blank=True, default=dict, verbose_name='Detected content')), + ('requested_actions', models.JSONField(blank=True, default=list, verbose_name='Requested actions')), + ('status', models.CharField(choices=[('awaiting_review', 'Awaiting review'), ('pending', 'Pending'), ('processing', 'Processing'), ('completed', 'Completed'), ('failed', 'Failed'), ('partial', 'Partial'), ('cancelled', 'Cancelled')], default='awaiting_review', max_length=20, verbose_name='Status')), + ('current_action', models.CharField(blank=True, choices=[('citation_markup', 'Mark citations'), ('xml_generation', 'Generate XML'), ('xml_validation', 'Validate XML'), ('sps_package_validation', 'Validate SPS package'), ('sps_package_generation', 'Generate SPS package'), ('html_generation', 'Generate HTML'), ('pdf_generation', 'Generate PDF')], max_length=32, verbose_name='Current action')), + ('error_message', models.TextField(blank=True, verbose_name='Last error')), + ('error_details', models.JSONField(blank=True, default=dict, verbose_name='Error details')), + ('error_traceback', models.TextField(blank=True, verbose_name='Traceback')), + ('retry_count', models.PositiveIntegerField(default=0, verbose_name='Attempts')), + ('processing_started_at', models.DateTimeField(blank=True, null=True, verbose_name='Started at')), + ('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='Completed at')), + ], + options={ + 'verbose_name': 'Processing', + 'verbose_name_plural': 'Processings', + 'ordering': ['-created'], + }, + ), + migrations.CreateModel( + name='ProcessingEvent', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Last update date')), + ('action', models.CharField(blank=True, choices=[('citation_markup', 'Mark citations'), ('xml_generation', 'Generate XML'), ('xml_validation', 'Validate XML'), ('sps_package_validation', 'Validate SPS package'), ('sps_package_generation', 'Generate SPS package'), ('html_generation', 'Generate HTML'), ('pdf_generation', 'Generate PDF')], max_length=32, verbose_name='Action')), + ('status', models.CharField(choices=[('pending', 'Pending'), ('running', 'Running'), ('completed', 'Completed'), ('failed', 'Failed'), ('skipped', 'Skipped')], default='pending', max_length=20, verbose_name='Status')), + ('message', models.TextField(blank=True, verbose_name='Message')), + ('details', models.JSONField(blank=True, default=dict, verbose_name='Details')), + ('task_id', models.CharField(blank=True, max_length=255, verbose_name='Task ID')), + ('started_at', models.DateTimeField(blank=True, null=True, verbose_name='Started at')), + ('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='Completed at')), + ], + options={ + 'verbose_name': 'Processing event', + 'verbose_name_plural': 'Processing trail', + 'ordering': ['processing', 'created'], + }, + ), + ] diff --git a/manuscripts/migrations/0002_initial.py b/manuscripts/migrations/0002_initial.py new file mode 100644 index 0000000..ea1994a --- /dev/null +++ b/manuscripts/migrations/0002_initial.py @@ -0,0 +1,224 @@ +# Generated by Django 6.0.5 on 2026-06-09 00:35 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('journals', '0002_initial'), + ('manuscripts', '0001_initial'), + ('references', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='article', + name='creator', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_creator', to=settings.AUTH_USER_MODEL, verbose_name='Creator'), + ), + migrations.AddField( + model_name='article', + name='issue', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='articles', to='journals.issue'), + ), + migrations.AddField( + model_name='article', + name='journal', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='articles', to='journals.journal'), + ), + migrations.AddField( + model_name='article', + name='updated_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_last_mod_user', to=settings.AUTH_USER_MODEL, verbose_name='Updater'), + ), + migrations.AddField( + model_name='articleartifact', + name='article', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='artifacts', to='manuscripts.article'), + ), + migrations.AddField( + model_name='articleartifact', + name='creator', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_creator', to=settings.AUTH_USER_MODEL, verbose_name='Creator'), + ), + migrations.AddField( + model_name='articleartifact', + name='updated_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_last_mod_user', to=settings.AUTH_USER_MODEL, verbose_name='Updater'), + ), + migrations.AddField( + model_name='articlereference', + name='creator', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_creator', to=settings.AUTH_USER_MODEL, verbose_name='Creator'), + ), + migrations.AddField( + model_name='articlereference', + name='reference', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='article_references', to='references.reference'), + ), + migrations.AddField( + model_name='articlereference', + name='selected_element', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='article_references', to='references.elementcitation'), + ), + migrations.AddField( + model_name='articlereference', + name='updated_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_last_mod_user', to=settings.AUTH_USER_MODEL, verbose_name='Updater'), + ), + migrations.AddField( + model_name='articlestructureversion', + name='article', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='structure_versions', to='manuscripts.article'), + ), + migrations.AddField( + model_name='articlestructureversion', + name='creator', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_creator', to=settings.AUTH_USER_MODEL, verbose_name='Creator'), + ), + migrations.AddField( + model_name='articlestructureversion', + name='updated_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_last_mod_user', to=settings.AUTH_USER_MODEL, verbose_name='Updater'), + ), + migrations.AddField( + model_name='articlereference', + name='structure', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='references', to='manuscripts.articlestructureversion'), + ), + migrations.AddField( + model_name='articleartifact', + name='structure', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='artifacts', to='manuscripts.articlestructureversion'), + ), + migrations.AddField( + model_name='citationoccurrence', + name='creator', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_creator', to=settings.AUTH_USER_MODEL, verbose_name='Creator'), + ), + migrations.AddField( + model_name='citationoccurrence', + name='references', + field=models.ManyToManyField(blank=True, related_name='citations', to='manuscripts.articlereference'), + ), + migrations.AddField( + model_name='citationoccurrence', + name='structure', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='citations', to='manuscripts.articlestructureversion'), + ), + migrations.AddField( + model_name='citationoccurrence', + name='updated_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_last_mod_user', to=settings.AUTH_USER_MODEL, verbose_name='Updater'), + ), + migrations.AddField( + model_name='processing', + name='articles', + field=models.ManyToManyField(blank=True, related_name='processings', to='manuscripts.article'), + ), + migrations.AddField( + model_name='processing', + name='creator', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_creator', to=settings.AUTH_USER_MODEL, verbose_name='Creator'), + ), + migrations.AddField( + model_name='processing', + name='updated_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_last_mod_user', to=settings.AUTH_USER_MODEL, verbose_name='Updater'), + ), + migrations.AddField( + model_name='articlestructureversion', + name='processing', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='structure_versions', to='manuscripts.processing'), + ), + migrations.AddField( + model_name='articleartifact', + name='processing', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='artifacts', to='manuscripts.processing'), + ), + migrations.CreateModel( + name='ArticleInput', + fields=[ + ], + options={ + 'verbose_name': 'DOCX', + 'verbose_name_plural': 'DOCX', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('manuscripts.processing',), + ), + migrations.CreateModel( + name='SPSPackageImport', + fields=[ + ], + options={ + 'verbose_name': 'SPS Package', + 'verbose_name_plural': 'SPS Packages', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('manuscripts.processing',), + ), + migrations.CreateModel( + name='XMLImport', + fields=[ + ], + options={ + 'verbose_name': 'XML', + 'verbose_name_plural': 'XML', + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('manuscripts.processing',), + ), + migrations.AddField( + model_name='processingevent', + name='article', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='events', to='manuscripts.article'), + ), + migrations.AddField( + model_name='processingevent', + name='creator', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_creator', to=settings.AUTH_USER_MODEL, verbose_name='Creator'), + ), + migrations.AddField( + model_name='processingevent', + name='processing', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='events', to='manuscripts.processing'), + ), + migrations.AddField( + model_name='processingevent', + name='updated_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_last_mod_user', to=settings.AUTH_USER_MODEL, verbose_name='Updater'), + ), + migrations.AddConstraint( + model_name='articlereference', + constraint=models.UniqueConstraint(fields=('structure', 'ref_id'), name='unique_structure_ref_id'), + ), + migrations.AddConstraint( + model_name='articlereference', + constraint=models.UniqueConstraint(fields=('structure', 'position'), name='unique_structure_ref_position'), + ), + migrations.AddConstraint( + model_name='articlestructureversion', + constraint=models.UniqueConstraint(fields=('article', 'version'), name='unique_article_structure_version'), + ), + migrations.AddIndex( + model_name='articleartifact', + index=models.Index(fields=['processing', 'article', 'artifact_type', 'is_current'], name='manuscripts_process_8a07b8_idx'), + ), + migrations.AddIndex( + model_name='processingevent', + index=models.Index(fields=['processing', 'article', 'action', 'status'], name='manuscripts_process_38c798_idx'), + ), + ] diff --git a/manuscripts/migrations/__init__.py b/manuscripts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/manuscripts/models/__init__.py b/manuscripts/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/manuscripts/models/article.py b/manuscripts/models/article.py new file mode 100644 index 0000000..df6c9dd --- /dev/null +++ b/manuscripts/models/article.py @@ -0,0 +1,249 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ +from modelcluster.models import ClusterableModel +from wagtail.admin.panels import FieldPanel, MultiFieldPanel +from wagtail.fields import StreamField +from wagtailautocomplete.edit_handlers import AutocompletePanel + +from core.choices import LANGUAGE +from core.forms import CoreAdminModelForm +from core.models import CommonControlField + +from manuscripts.blocks import ( + AffParagraphBlock, + AuthorParagraphBlock, + CompoundParagraphBlock, + ImageBlock, + ParagraphBlock, + ParagraphWithLanguageBlock, + RefParagraphBlock, + TableBlock, +) +from manuscripts.choices import ( + ArtifactType, + InputType, + ProcessStatus, +) + + +class Article(CommonControlField, ClusterableModel): + title = models.TextField(_("Provisional title"), blank=True) + doi = models.CharField(_("DOI"), max_length=255, blank=True, db_index=True) + pid = models.CharField(_("PID"), max_length=255, blank=True, db_index=True) + content_checksum = models.CharField(_("Content checksum"), max_length=64, blank=True, db_index=True) + journal = models.ForeignKey( + "journals.Journal", on_delete=models.SET_NULL, related_name="articles", null=True, blank=True + ) + issue = models.ForeignKey( + "journals.Issue", on_delete=models.SET_NULL, related_name="articles", null=True, blank=True + ) + status = models.CharField( + _("Status"), max_length=20, choices=ProcessStatus.choices, default=ProcessStatus.PENDING + ) + language = models.CharField(_("Language"), max_length=10, choices=LANGUAGE, blank=True, default="en") + license = models.URLField(_("License (URL)"), max_length=500, blank=True, null=True) + fpage = models.CharField(_("First page"), max_length=32, blank=True) + lpage = models.CharField(_("Last page"), max_length=32, blank=True) + seq = models.CharField(_("Sequence"), max_length=32, blank=True) + elocatid = models.CharField(_("Elocation ID"), max_length=255, blank=True) + artdate = models.DateField(_("Publication date"), null=True, blank=True) + ahpdate = models.DateField(_("AHP date"), null=True, blank=True) + metadata = models.JSONField(_("Metadata"), default=dict, blank=True) + + panels = [ + MultiFieldPanel([FieldPanel("title"), FieldPanel("doi"), FieldPanel("pid")], heading=_("Article")), + MultiFieldPanel([AutocompletePanel("journal"), AutocompletePanel("issue")], heading=_("Editorial link")), + MultiFieldPanel([ + FieldPanel("language"), + FieldPanel("license"), + FieldPanel("fpage"), + FieldPanel("lpage"), + FieldPanel("seq"), + FieldPanel("elocatid"), + FieldPanel("artdate"), + FieldPanel("ahpdate"), + ], heading=_("Article metadata")), + FieldPanel("status", read_only=True), + ] + base_form_class = CoreAdminModelForm + + def current_artifact(self, artifact_type): + return self.artifacts.filter(artifact_type=artifact_type, is_current=True).first() + + @property + def current_structure(self): + return self.structure_versions.filter(is_current=True).first() + + def __str__(self): + return self.title or self.doi or self.pid or f"Article {self.pk}" + + class Meta: + verbose_name = _("Article") + verbose_name_plural = _("Articles") + ordering = ["-created"] + + +class ArticleStructureVersion(CommonControlField): + article = models.ForeignKey( + Article, on_delete=models.CASCADE, related_name="structure_versions" + ) + processing = models.ForeignKey( + "Processing", + on_delete=models.SET_NULL, + related_name="structure_versions", + null=True, + blank=True, + ) + version = models.PositiveIntegerField(_("Version"), default=1) + is_current = models.BooleanField(_("Current version"), default=True) + source_kind = models.CharField( + _("Origin"), max_length=32, choices=InputType.choices, default=InputType.DOCUMENT + ) + front = StreamField( + [ + ("paragraph_with_language", ParagraphWithLanguageBlock()), + ("paragraph", ParagraphBlock()), + ("author_paragraph", AuthorParagraphBlock()), + ("aff_paragraph", AffParagraphBlock()), + ], + blank=True, + ) + body = StreamField( + [ + ("paragraph", ParagraphBlock()), + ("paragraph_with_language", ParagraphWithLanguageBlock()), + ("compound_paragraph", CompoundParagraphBlock()), + ("image", ImageBlock()), + ("table", TableBlock()), + ], + blank=True, + ) + back = StreamField( + [ + ("paragraph", ParagraphBlock()), + ("ref_paragraph", RefParagraphBlock()), + ], + blank=True, + ) + base_xml = models.TextField(_("Preserved base XML"), blank=True) + roundtrip_warnings = models.JSONField(_("Round-trip warnings"), default=list, blank=True) + xref_status = models.JSONField(_("Citation status"), default=dict, blank=True) + panels = [ + MultiFieldPanel( + [FieldPanel("version", read_only=True), FieldPanel("source_kind", read_only=True)], + heading=_("Structural version"), + ), + FieldPanel("front"), + FieldPanel("body"), + FieldPanel("back"), + FieldPanel("roundtrip_warnings", read_only=True), + FieldPanel("xref_status", read_only=True), + ] + + def __str__(self): + return f"{self.article} - structure v{self.version}" + + class Meta: + verbose_name = _("Article structural version") + verbose_name_plural = _("Article structural versions") + ordering = ["article", "-version"] + constraints = [ + models.UniqueConstraint( + fields=["article", "version"], name="unique_article_structure_version" + ) + ] + + +class ArticleReference(CommonControlField): + structure = models.ForeignKey( + ArticleStructureVersion, on_delete=models.CASCADE, related_name="references" + ) + reference = models.ForeignKey( + "references.Reference", + on_delete=models.SET_NULL, + related_name="article_references", + null=True, + blank=True, + ) + selected_element = models.ForeignKey( + "references.ElementCitation", + on_delete=models.SET_NULL, + related_name="article_references", + null=True, + blank=True, + ) + position = models.PositiveIntegerField(_("Order")) + ref_id = models.CharField(_("SPS ID"), max_length=32) + mixed_citation = models.TextField(_("Original reference")) + metadata = models.JSONField(_("Structured data"), default=dict, blank=True) + + def __str__(self): + return f"{self.ref_id}: {self.mixed_citation[:80]}" + + class Meta: + verbose_name = _("Article reference") + verbose_name_plural = _("Article references") + ordering = ["structure", "position"] + constraints = [ + models.UniqueConstraint(fields=["structure", "ref_id"], name="unique_structure_ref_id"), + models.UniqueConstraint( + fields=["structure", "position"], name="unique_structure_ref_position" + ), + ] + + +class CitationOccurrence(CommonControlField): + class Status(models.TextChoices): + LINKED = "linked", _("Linked") + ORPHAN = "orphan", _("Orphan") + AMBIGUOUS = "ambiguous", _("Ambiguous") + MANUAL = "manual", _("Manually corrected") + + structure = models.ForeignKey( + ArticleStructureVersion, on_delete=models.CASCADE, related_name="citations" + ) + text = models.TextField(_("In-text citation")) + location = models.JSONField(_("Location"), default=dict, blank=True) + status = models.CharField(_("Status"), max_length=16, choices=Status.choices) + references = models.ManyToManyField(ArticleReference, related_name="citations", blank=True) + + def __str__(self): + return self.text + + class Meta: + verbose_name = _("Citation occurrence") + verbose_name_plural = _("Citation occurrences") + ordering = ["structure", "created"] + + +class ArticleArtifact(CommonControlField): + processing = models.ForeignKey("Processing", on_delete=models.CASCADE, related_name="artifacts") + article = models.ForeignKey( + Article, on_delete=models.CASCADE, related_name="artifacts", null=True, blank=True + ) + structure = models.ForeignKey( + ArticleStructureVersion, + on_delete=models.SET_NULL, + related_name="artifacts", + null=True, + blank=True, + ) + artifact_type = models.CharField(_("Type"), max_length=32, choices=ArtifactType.choices) + file = models.FileField(_("File"), upload_to="processings/artifacts/%Y/%m/%d/") + original_path = models.CharField(_("Original path"), max_length=1024, blank=True) + source_url = models.URLField(_("Source URL"), max_length=2048, blank=True) + version = models.PositiveIntegerField(_("Version"), default=1) + is_current = models.BooleanField(_("Current version"), default=True) + file_size_bytes = models.PositiveBigIntegerField(_("Size"), default=0) + checksum = models.CharField(_("SHA256"), max_length=64, blank=True, db_index=True) + metadata = models.JSONField(_("Metadata"), default=dict, blank=True) + is_stale = models.BooleanField(_("Outdated"), default=False) + + def __str__(self): + return f"{self.get_artifact_type_display()} v{self.version}" + + class Meta: + verbose_name = _("Artifact") + verbose_name_plural = _("Artifacts") + ordering = ["processing", "article", "artifact_type", "-version"] + indexes = [models.Index(fields=["processing", "article", "artifact_type", "is_current"])] diff --git a/manuscripts/models/processing.py b/manuscripts/models/processing.py new file mode 100644 index 0000000..a75284d --- /dev/null +++ b/manuscripts/models/processing.py @@ -0,0 +1,133 @@ +from django.db import models +from django.utils.translation import gettext_lazy as _ +from wagtail.admin.panels import FieldPanel, MultiFieldPanel + +from core.forms import CoreAdminModelForm +from core.models import CommonControlField + +from manuscripts.choices import EventStatus, InputType, ProcessingAction, ProcessStatus + + +class Processing(CommonControlField): + title = models.CharField(_("Identification"), max_length=255, blank=True) + input_file = models.FileField(_("File to be uploaded"), upload_to="processings/input/%Y/%m/%d/") + input_checksum = models.CharField(_("SHA256"), max_length=64, blank=True, db_index=True) + detected_type = models.CharField( + _("Detected type"), max_length=32, choices=InputType.choices, default=InputType.UNKNOWN + ) + confirmed_type = models.CharField( + _("Confirmed type"), max_length=32, choices=InputType.choices, default=InputType.UNKNOWN + ) + inspection = models.JSONField(_("Detected content"), default=dict, blank=True) + requested_actions = models.JSONField(_("Requested actions"), default=list, blank=True) + status = models.CharField( + _("Status"), max_length=20, choices=ProcessStatus.choices, default=ProcessStatus.AWAITING_REVIEW + ) + current_action = models.CharField( + _("Current action"), max_length=32, choices=ProcessingAction.choices, blank=True + ) + articles = models.ManyToManyField("Article", related_name="processings", blank=True) + error_message = models.TextField(_("Last error"), blank=True) + error_details = models.JSONField(_("Error details"), default=dict, blank=True) + error_traceback = models.TextField(_("Traceback"), blank=True) + retry_count = models.PositiveIntegerField(_("Attempts"), default=0) + processing_started_at = models.DateTimeField(_("Started at"), null=True, blank=True) + completed_at = models.DateTimeField(_("Completed at"), null=True, blank=True) + panels = [ + MultiFieldPanel([FieldPanel("title"), FieldPanel("input_file")], heading=_("Input")), + MultiFieldPanel( + [FieldPanel("detected_type", read_only=True), FieldPanel("confirmed_type"), FieldPanel("requested_actions")], + heading=_("Review"), + ), + MultiFieldPanel( + [FieldPanel("status", read_only=True), FieldPanel("current_action", read_only=True), FieldPanel("error_message", read_only=True)], + heading=_("Execution"), + ), + ] + base_form_class = CoreAdminModelForm + + @property + def article_count(self): + return self.articles.count() + + def __str__(self): + return self.title or self.input_file.name.rsplit("/", 1)[-1] + + class Meta: + verbose_name = _("Processing") + verbose_name_plural = _("Processings") + ordering = ["-created"] + + +class ProcessingEvent(CommonControlField): + processing = models.ForeignKey(Processing, on_delete=models.CASCADE, related_name="events") + article = models.ForeignKey( + "Article", on_delete=models.CASCADE, related_name="events", null=True, blank=True + ) + action = models.CharField(_("Action"), max_length=32, choices=ProcessingAction.choices, blank=True) + status = models.CharField(_("Status"), max_length=20, choices=EventStatus.choices, default=EventStatus.PENDING) + message = models.TextField(_("Message"), blank=True) + details = models.JSONField(_("Details"), default=dict, blank=True) + task_id = models.CharField(_("Task ID"), max_length=255, blank=True) + started_at = models.DateTimeField(_("Started at"), null=True, blank=True) + completed_at = models.DateTimeField(_("Completed at"), null=True, blank=True) + + def __str__(self): + return f"{self.get_action_display() or 'Input'} - {self.get_status_display()}" + + class Meta: + verbose_name = _("Processing event") + verbose_name_plural = _("Processing trail") + ordering = ["processing", "created"] + indexes = [models.Index(fields=["processing", "article", "action", "status"])] + + +class ArticleInputManager(models.Manager): + def get_queryset(self): + return super().get_queryset().filter( + models.Q(confirmed_type__in=[InputType.DOCUMENT, InputType.SOURCE_PACKAGE]) | + (models.Q(confirmed_type=InputType.UNKNOWN) & models.Q(detected_type__in=[InputType.DOCUMENT, InputType.SOURCE_PACKAGE])) + ) + + +class XMLImportManager(models.Manager): + def get_queryset(self): + return super().get_queryset().filter( + models.Q(confirmed_type=InputType.XML) | + (models.Q(confirmed_type=InputType.UNKNOWN) & models.Q(detected_type=InputType.XML)) + ) + + +class SPSPackageImportManager(models.Manager): + def get_queryset(self): + return super().get_queryset().filter( + models.Q(confirmed_type__in=[InputType.SPS_PACKAGE, InputType.AMBIGUOUS_ZIP]) | + (models.Q(confirmed_type=InputType.UNKNOWN) & models.Q(detected_type__in=[InputType.SPS_PACKAGE, InputType.AMBIGUOUS_ZIP])) + ) + + +class ArticleInput(Processing): + objects = ArticleInputManager() + + class Meta: + proxy = True + verbose_name = _("DOCX") + verbose_name_plural = _("DOCX") + + +class XMLImport(Processing): + objects = XMLImportManager() + + class Meta: + proxy = True + verbose_name = _("XML") + verbose_name_plural = _("XML") + + +class SPSPackageImport(Processing): + objects = SPSPackageImportManager() + + class Meta: + proxy = True + verbose_name = _("SPS Package") + verbose_name_plural = _("SPS Packages") diff --git a/manuscripts/processing.py b/manuscripts/processing.py new file mode 100644 index 0000000..f9ff418 --- /dev/null +++ b/manuscripts/processing.py @@ -0,0 +1,145 @@ +import os +import traceback +from pathlib import PurePosixPath + +from django.utils import timezone + +from .artifacts import extract_docx_assets, resolve_article_assets +from .choices import ( + ArtifactType, + EventStatus, + InputType, + ProcessingAction, + ProcessStatus, +) +from .controller import get_or_create_article +from .models.processing import Processing, ProcessingEvent +from .utils.ingestion import ingest_document, ingest_xml, ingest_zip +from .utils.inspection import ACTION_ARTIFACT_TYPES, resolve_actions +from .utils.processing_actions import run_action + + +def process_input(task_instance, processing_id, start_action=None): + processing = _begin_processing(processing_id) + try: + _ingest_input(processing) + actions = _resolve_pipeline_actions(processing, start_action) + _record_skipped_actions(processing, actions) + partial = _execute_actions(processing, actions, task_instance) + _complete_processing(processing, partial) + except Exception as exc: + _fail_processing(processing, exc) + raise + + +def _begin_processing(processing_id): + processing = Processing.objects.get(pk=processing_id) + processing.status = ProcessStatus.PROCESSING + processing.processing_started_at = timezone.now() + processing.completed_at = None + processing.error_message = "" + processing.error_details = {} + processing.error_traceback = "" + processing.save() + return processing + + +def _ingest_input(processing): + if not processing.confirmed_type or processing.confirmed_type == InputType.UNKNOWN: + raise ValueError("Confirme o tipo de entrada antes de iniciar.") + + if processing.confirmed_type in (InputType.DOCUMENT, InputType.SOURCE_PACKAGE): + source = ingest_document(processing) + article, _metadata = get_or_create_article(processing, title=PurePosixPath(source.file.name).stem) + processing.artifacts.filter( + article__isnull=True, artifact_type=ArtifactType.ASSET + ).update(article=article) + extract_docx_assets(processing, source, article) + + elif processing.confirmed_type == InputType.XML: + with processing.input_file.open("rb") as source: + article, xml_artifact = ingest_xml( + processing, os.path.basename(processing.input_file.name), source.read() + ) + resolve_article_assets( + processing, article, xml_artifact.metadata.get("referenced_assets", []) + ) + + elif processing.confirmed_type == InputType.SPS_PACKAGE: + _source, _xmls = ingest_zip(processing) + + else: + raise ValueError("Tipo de entrada não processável.") + + +def _resolve_pipeline_actions(processing, start_action): + actions = resolve_actions(processing.requested_actions, processing.confirmed_type) + applicable = resolve_actions( + [value for value, _label in ProcessingAction.choices], + processing.confirmed_type, + ) + + if not start_action: + return actions + + if start_action not in applicable: + raise ValueError("A etapa escolhida não se aplica a este processamento.") + + actions = applicable[applicable.index(start_action):] + invalidated_types = { + artifact_type + for action in actions + for artifact_type in ACTION_ARTIFACT_TYPES.get(action, []) + } + processing.artifacts.filter( + artifact_type__in=invalidated_types, is_current=True + ).update(is_current=False, is_stale=True) + return actions + + +def _record_skipped_actions(processing, actions): + applicable = resolve_actions( + [value for value, _label in ProcessingAction.choices], + processing.confirmed_type, + ) + for action in applicable: + if action not in actions: + ProcessingEvent.objects.create( + processing=processing, + action=action, + status=EventStatus.SKIPPED, + message="Ação não selecionada para esta execução.", + completed_at=timezone.now(), + ) + + +def _execute_actions(processing, actions, task_instance): + partial = False + for action in actions: + task_id = task_instance.request.id if task_instance else None + partial = run_action(processing, action, task_id) or partial + return partial + + +def _complete_processing(processing, partial): + partial = partial or processing.events.filter( + status=EventStatus.FAILED, details__severity="warning" + ).exists() + processing.status = ProcessStatus.PARTIAL if partial else ProcessStatus.COMPLETED + processing.completed_at = timezone.now() + processing.current_action = "" + processing.save() + processing.articles.update(status=processing.status) + + +def _fail_processing(processing, exc): + processing.status = ProcessStatus.FAILED + processing.error_message = str(exc) + processing.error_details = {"action": processing.current_action, "type": exc.__class__.__name__} + processing.error_traceback = traceback.format_exc() + processing.save() + processing.events.filter(status=EventStatus.RUNNING).update( + status=EventStatus.FAILED, + message=str(exc), + completed_at=timezone.now(), + ) diff --git a/manuscripts/static/css/article.css b/manuscripts/static/css/article.css new file mode 100644 index 0000000..07c1ba4 --- /dev/null +++ b/manuscripts/static/css/article.css @@ -0,0 +1,4 @@ +@charset "UTF-8";/*! + * Article + */@-webkit-keyframes bounce{0%,100%,20%,50%,80%{-webkit-transform:translateY(0)}40%{-webkit-transform:translateY(30px)}60%{-webkit-transform:translateY(15px)}}@-moz-keyframes bounce{0%,100%,20%,50%,80%{-moz-transform:translateY(0)}40%{-moz-transform:translateY(30px)}60%{-moz-transform:translateY(15px)}}@-ms-keyframes bounce{0%,100%,20%,50%,80%{-ms-transform:translateY(0)}40%{-ms-transform:translateY(30px)}60%{-ms-transform:translateY(15px)}}@-o-keyframes bounce{0%,100%,20%,50%,80%{-o-transform:translateY(0)}40%{-o-transform:translateY(30px)}60%{-o-transform:translateY(15px)}}@keyframes bounce{0%,100%,20%,50%,80%{transform:translateY(0)}40%{transform:translateY(30px)}60%{transform:translateY(15px)}}@-webkit-keyframes slideInUp{from{-webkit-transform:translate3d(0,30%,0);transform:translate3d(0,30%,0);visibility:visible;opacity:0}to{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}.scielo__shadow-1{box-shadow:0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24)}.scielo__shadow-2{box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23)}.scielo__shadow-3{box-shadow:0 10px 20px rgba(0,0,0,.19),0 6px 6px rgba(0,0,0,.23)}.scielo__shadow-4{box-shadow:0 14px 28px rgba(0,0,0,.25),0 10px 10px rgba(0,0,0,.22)}.scielo__shadow-5{box-shadow:0 19px 38px rgba(0,0,0,.3),0 15px 12px rgba(0,0,0,.22)}.article .zindexFix{z-index:98!important}.article a.goto{white-space:nowrap;text-decoration:none}.article a.goto .glyphBtn{margin-right:-5px}.article .levelMenu{padding:18px 0;margin:0;height:100px}.article .levelMenu a.selected:after{border-bottom-color:#fff;display:none}.article .levelMenu .downloadOptions li,.article .levelMenu .downloadOptions ul{display:inline;margin:0;padding:0}.article .levelMenu .downloadOptions li{list-style:none}.article .levelMenu .downloadOptions ul.dropdown-menu{display:none;min-width:inherit;width:100%;border-color:#dedddb;font-size:.9em;border-top-left-radius:0;border-top-right-radius:0;border-top:0}.article .levelMenu .downloadOptions .group:hover ul.dropdown-menu{display:block}.article .levelMenu .downloadOptions .group:hover a.btn{color:#fff}.article .levelMenu .downloadOptions .group:hover a.btn .glyphBtn.pdfDownload{background-position:center -2800px}.article .levelMenu .downloadOptions .group:hover a.btn .glyphBtn.xmlDownload{background-position:center -2845px}.article .levelMenu .downloadOptions .group:hover a.btn .glyphBtn.epubDownload{background-position:center -3700px}.article .levelMenu .downloadOptions .btn-group .group:not(:first-child):not(:last-child) .btn{border-radius:0}.article .levelMenu .downloadOptions .btn-group .group:first-child:not(:last-child) .btn{border-bottom-right-radius:0;border-top-right-radius:0}.article .levelMenu .downloadOptions .btn-group .group:first-child:not(:last-child) .btn{border-bottom-right-radius:0;border-top-right-radius:0}.article .levelMenu .downloadOptions .btn-group .group:last-child:not(:first-child) .btn{border-bottom-left-radius:0;border-top-left-radius:0}.article .levelMenu .downloadOptions .group{display:block;float:left;position:relative}.article .levelMenu .downloadOptions .group a.btn{width:100%}.article .levelMenu .downloadOptions .group+.group{margin-left:-1px}.article .levelMenu .downloadOptions .btn{text-align:left}.article .share{display:flex;justify-content:flex-end;align-items:center;height:36px}.article .share a{margin:0 3px}.article .share .sendViaMail{margin-left:4px}.article .journalMenu .language{top:10px}.article .alternativeHeader{top:0!important}.article .alternativeHeader .mainNav{height:55px}.article .mainNav{height:55px}.article .mainMenu{top:-7px}.article .xref{display:inline-block;font-weight:700;text-align:center;color:#3867ce;cursor:pointer}.scielo__theme--dark .article .xref{color:#86acff}.scielo__theme--light .article .xref{color:#3867ce}.article .xref.big{margin-top:0;vertical-align:middle;color:#b67f00}.article .xref a{text-decoration:none;color:#b67f00}.article sup.xref{padding:4px 0 3px}.article .ref{position:relative;display:inline}@media screen and (max-width:575px){.article .ref{position:static}}.article .ref .refCtt{-webkit-box-shadow:2px 2px 7px 0 rgba(0,0,0,.2);-moz-box-shadow:2px 2px 7px 0 rgba(0,0,0,.2);box-shadow:2px 2px 7px 0 rgba(0,0,0,.2)}.article .ref .closed{display:none}.article .ref .opened{margin-top:1.4em;padding:14px;position:absolute;width:350px;height:auto!important;overflow-y:inherit!important;overflow-x:hidden;text-overflow:ellipsis;border-radius:4px;z-index:99;background:#3867ce;color:#fff}@media screen and (max-width:575px){.article .ref .opened{width:90%}}.scielo__theme--dark .article .ref .opened{background:#86acff;color:#333}.scielo__theme--light .article .ref .opened{background:#3867ce;color:#fff}.article .ref .opened:before{content:'';display:block;width:100%;position:absolute;height:.6em;margin-top:-1.6em;background:0 0;left:0}.article .ref .opened a{color:#fff!important}.article .ref .opened a:hover{text-decoration:underline}.article .ref .opened strong{display:block;margin:0 0 5px}.article .ref .opened .source{display:block;margin-top:5px}.article .ref .opened .refOverflow{overflow-x:hidden;text-overflow:ellipsis}.article .ref.footnote{letter-spacing:0}.article .ref.footnote .refCtt{padding:0}.article .ref.footnote .refCtt .refCttPadding{display:block;padding:14px}.article .ref.footnote .refCtt.opened{background:#fef5e8;border:1px solid #fce0b7;color:#333;padding:5px 10px}.article .ref.footnote .fn-title{display:block;text-transform:uppercase;color:#b67f00}.article .ref.footnote .footref{cursor:default}.article .ref.footnote .smallRef{font-size:1em;position:relative;display:block;padding:14px;width:100%;color:#fff;border:0;border-radius:0}.article .ref.footnote .smallRef .xref{position:absolute;top:12px;cursor:default}.article .ref.footnote .smallRef .xref:first-child{font-size:11px!important}.article .ref.footnote .smallRef .footrefCtt{display:block;padding-left:14px}.article .refList{margin:0;padding:0;width:100%}.article .refList *{line-height:130%}.article .refList [class*=" material-icons"],.article .refList [class^=material-icons]{line-height:1}.article .refList a{overflow-x:hidden;text-overflow:ellipsis}.article .refList.outer{padding-bottom:10px;overflow:hidden;-webkit-box-shadow:inset 0 -7px 7px -7px rgba(0,0,0,.2);box-shadow:inset 0 -7px 7px -7px rgba(0,0,0,.2)}.article .refList.full{position:absolute;height:auto!important;overflow:inherit!important;background:#fff;z-index:99;padding-bottom:0;-webkit-box-shadow:0 0 10px 0 rgba(0,0,0,.2);box-shadow:0 0 10px 0 rgba(0,0,0,.2)}.article .refList li{list-style:none;padding:16px 8px 16px 0;margin:0;width:100%;border-bottom:1px dotted #ccc}.scielo__theme--dark .article .refList li{border-bottom:1px dotted rgba(255,255,255,.3)}.scielo__theme--light .article .refList li{border-bottom:1px dotted #ccc}.article .refList li:last-child{background:0 0}.article .refList li:after{content:'';clear:both;display:block;height:1px;float:none;width:100%}.article .refList li.highlight{background-color:#f0f3fb}.article .refList li.highlight .closed{display:none}.article .refList li.highlight .opened{display:inline-block}.article .refList li strong{margin:0 0 10px}.article .refList sup{border-radius:30px}.article .refList .source{font-style:italic}.article .refList.footnote .xref.big{color:#b67f00}.article .ref-list .refList .xref{width:33px;padding:5px 10px;cursor:default;position:absolute;left:0;top:5px;margin-top:1%}.article .ref-list .refList .refCtt.opened{margin-top:1.4em}.article .ref-list .refList li{position:relative;padding-left:30px;text-overflow:ellipsis;z-index:1}.article .ref-list .refList div{display:block;overflow-wrap:break-word;word-wrap:break-word;-ms-word-break:break-all;word-break:break-all;word-break:break-word;-ms-hyphens:auto;-moz-hyphens:auto;-webkit-hyphens:auto}.article .ref-list .refList div strong{display:inline}.article .ref-list .refList.footnote li{padding:0 8px 8px 0}.article .ref-list .refList.footnote li .xref.big{position:static;width:auto}.articleCtt{background:#f7f6f4}.scielo__theme--dark .articleCtt{background:#393939}.scielo__theme--light .articleCtt{background:#f7f6f4}.articleCtt hr{border:0;background:url(../img/dashline.png) bottom left repeat-x;height:1px;margin:50px 0}.articleCtt .sci-ico-fileFigure:before,.articleCtt .sci-ico-fileFormula:before,.articleCtt .sci-ico-fileTable:before{margin-left:-2px;margin-right:-4px}.articleCtt .open-asset-modal{white-space:nowrap}.articleCtt .container{position:relative}.articleCtt .articleBlock h1{margin:0;font-size:1.9em}.articleCtt .articleBlock a:active,.articleCtt .articleBlock a:focus,.articleCtt .articleBlock a:visited{text-decoration:none}.articleCtt .articleMeta,.articleCtt .editionMeta{text-align:center;font-size:.85em}.articleCtt .articleMeta span,.articleCtt .editionMeta span{font-size:1em;margin:0;font-weight:400;color:#a7a49e}.articleCtt .articleMeta .atricleLink,.articleCtt .editionMeta .atricleLink{width:18px;background-position:center -2576px}.articleCtt .articleMeta{line-height:24px}.articleCtt .articleMeta .sci-ico-cr,.articleCtt .articleMeta .sci-ico-public-domain{font-size:21px}.articleCtt .articleMeta label{margin-left:8px;width:10%;border:1px solid #e0e0df;cursor:pointer}.articleCtt .articleMeta div{display:inline}.articleCtt .articleMeta div:first-child{margin-right:60px}.articleCtt .articleMeta .doi{color:#1b92e4}.articleCtt .license{letter-spacing:-16.28px;vertical-align:middle;line-height:44px;white-space:nowrap;display:inline-block;margin-bottom:5px;cursor:pointer}.articleCtt .license [class*=" sci-ico-"],.articleCtt .license [class^=sci-ico-]{cursor:pointer;font-size:44px}.articleCtt .license [class*=" sci-ico-"].sci-ico-cc,.articleCtt .license [class^=sci-ico-].sci-ico-cc{margin-right:4px}.articleCtt .contribGroup{color:#403d39;margin:15px 10%;font-size:1.1em;text-align:center}.articleCtt .contribGroup a.btn-fechar{display:inline-block;border-radius:100%;cursor:pointer;width:30px;height:30px;font-size:86%;padding:5px 0;text-align:center;margin-top:10px}.articleCtt .contribGroup a.btn-fechar:hover{color:#fff}.articleCtt .contribGroup .sci-ico-emailOutlined{font-size:20px;vertical-align:baseline}.articleCtt .contribGroup .dropdown{display:inline-block;padding:0 10px}.articleCtt .contribGroup .dropdown .dropdown-toggle{white-space:nowrap}.articleCtt .contribGroup .dropdown .dropdown-menu{padding:0 20px 10px 20px;color:#fff;text-align:left;box-shadow:none;border:none}.articleCtt .contribGroup .dropdown .dropdown-menu strong{display:block;margin:20px 0 8px 0;font-size:11px;color:#00314c;text-transform:uppercase}.articleCtt .contribGroup .dropdown a{cursor:pointer}.articleCtt .contribGroup .dropdown a span{display:inline-block;padding:5px 0}.articleCtt .contribGroup .dropdown.open a{color:#fff}.articleCtt .contribGroup.contribGroupAlignLeft{text-align:left;margin-left:0;margin-top:0}.articleCtt .contribGroup.contribGroupAlignLeft .dropdown:first-child{margin-left:-10px}.articleCtt .linkGroup{position:relative;font-size:.85em}.articleCtt .linkGroup a.selected{position:relative}.articleCtt .linkGroup a.selected:after{content:'';display:block;position:absolute;bottom:-16px;left:4px;width:16px;height:7px;background:url(../img/articleContent-arrow.png) bottom center no-repeat;z-index:999}.articleCtt .floatInformation{margin-top:9px;border:1px solid #ddd;padding:15px;position:absolute;display:none;z-index:99;width:100%;background:#f7f6f4}.scielo__theme--dark .articleCtt .floatInformation{background:#393939}.scielo__theme--light .articleCtt .floatInformation{background:#f7f6f4}.articleCtt .floatInformation .close{margin-top:-7px}.articleCtt .floatInformation ul{margin:0;padding:0}.articleCtt .floatInformation li{list-style:none;margin-bottom:7px;padding-left:20px}.articleCtt .floatInformation li .xref:first-child{margin-left:-22px}.articleCtt .floatInformation .rowBlock{padding:7px 15px;background:url(../img/dashline.png) bottom left repeat-x}.articleCtt .floatInformation .rowBlock:last-child{background:0 0}.articleCtt .floatInformation h3{margin:0 0 10px}.articleCtt .articleTxt{position:relative;padding:0 50px 100px;margin-bottom:60px;overflow-x:hidden;box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23);border-radius:4px;background:#fff}.scielo__theme--dark .articleCtt .articleTxt{background:#333}.scielo__theme--light .articleCtt .articleTxt{background:#fff}.articleCtt .articleTxt .article-title,.articleCtt .articleTxt .articleSectionTitle{margin:25px 0 12px}@media screen and (max-width:575px){.articleCtt .articleTxt .article-title,.articleCtt .articleTxt .articleSectionTitle{font-weight:700;font-size:1.75rem;line-height:1.2em;letter-spacing:-.14px}}.articleCtt .articleTxt .articleBadge-editionMeta-doi-copyLink{font-size:.85em;margin:0;font-weight:400;padding:10px 0 0;text-align:center;min-height:35px;line-height:110%;color:#6c6b6b}.scielo__theme--dark .articleCtt .articleTxt .articleBadge-editionMeta-doi-copyLink{color:#adadad}.scielo__theme--light .articleCtt .articleTxt .articleBadge-editionMeta-doi-copyLink{color:#6c6b6b}.articleCtt .articleTxt .articleBadge-editionMeta-doi-copyLink ._articleBadge{font-weight:700;opacity:1}.articleCtt .articleTxt .articleBadge-editionMeta-doi-copyLink .group-doi{white-space:nowrap;display:inline-block}.articleCtt .articleTxt .articleBadge-editionMeta-doi-copyLink ._doi{color:#3867ce}@media screen and (max-width:575px){.articleCtt .articleTxt .articleBadge-editionMeta-doi-copyLink ._doi{display:table;white-space:pre-wrap;margin:12px 0}}.scielo__theme--dark .articleCtt .articleTxt .articleBadge-editionMeta-doi-copyLink ._doi{color:#86acff}.scielo__theme--light .articleCtt .articleTxt .articleBadge-editionMeta-doi-copyLink ._doi{color:#3867ce}.articleCtt .articleTxt .articleBadge-editionMeta-doi-copyLink .copyLink{white-space:nowrap;margin:0 0 8px 8px}.articleCtt .articleTxt .article-title{text-align:center}@media screen and (max-width:575px){.articleCtt .articleTxt .article-title{font-weight:700;font-size:1.75rem;line-height:1.2em;letter-spacing:-.14px}}.articleCtt .articleTxt .article-title .sci-ico-openAccess{margin-bottom:7px}.articleCtt .articleTxt .article-title .short-link{position:relative;visibility:hidden;cursor:pointer;text-decoration:none}.articleCtt .articleTxt .article-title .short-link [class^=sci-ico-]{vertical-align:baseline!important;margin-left:.5rem}.articleCtt .articleTxt .article-title .short-link:after{position:absolute;background:#34ad65;top:100%;left:0;right:0;bottom:0;z-index:2;text-align:center;font-family:scielo-glyphs!important;content:"\e924";color:#fff;font-size:20px;visibility:hidden;vertical-align:middle;display:flex;justify-content:center;align-items:center;border-radius:4px}.articleCtt .articleTxt .article-title .short-link.copyFeedback:after{top:0;visibility:visible}.articleCtt .articleTxt .article-title .ref .opened{margin-top:2.4em}.articleCtt .articleTxt .article-title .ref.footnote .xref:first-child{font-size:1.5rem}.articleCtt .articleTxt .article-title .ref.footnote .refCtt.opened{text-align:left;font-weight:400;font-size:18px;letter-spacing:inherit;background:#fef5e8;border:1px solid #fce0b7;color:#403d39}.articleCtt .articleTxt .article-title .ref.footnote .refCtt .refCttPadding{line-height:1.5rem}.articleCtt .articleTxt .article-title:hover .short-link{visibility:visible}.articleCtt .articleTxt h2.article-title{font-weight:400}.articleCtt .articleTxt .article-correction-title{margin:10px 15% 20px;border:2px solid #f5d431;padding:20px}.articleCtt .articleTxt .article-correction-title .panel-heading{font-size:13px;font-weight:700;text-align:left;padding:3px;padding:5px}.articleCtt .articleTxt .article-correction-title .panel-body{padding:0}.articleCtt .articleTxt .article-correction-title ul{margin:0;padding:0;text-align:left;font-size:14px}.articleCtt .articleTxt .article-correction-title li{list-style:none;padding-left:15px;position:relative}.articleCtt .articleTxt .article-correction-title li:before{content:'\00bb';font-weight:700;position:absolute;left:0}.articleCtt .articleTxt .article-correction-title a{font-weight:700}.articleCtt .articleTxt .article-correction-title a:hover{text-decoration:underline}.articleCtt .articleTxt .articleSection{padding:0 0 1px;background:url(../img/dashline.png) bottom left repeat-x}.articleCtt .articleTxt .articleSection .article-title{text-align:left}@media screen and (max-width:575px){.articleCtt .articleTxt .articleSection .article-title{font-weight:700;font-size:1.75rem;line-height:1.2em;letter-spacing:-.14px}}.articleCtt .articleTxt .articleSection:last-child{background:0 0}.articleCtt .articleTxt .articleSection .articleSignature{font-size:15px;font-style:italic}.articleCtt .articleTxt .articleSection .articleSignature small{display:block}.articleCtt .articleTxt .articleSection.articleSection--abstract h3,.articleCtt .articleTxt .articleSection.articleSection--resumen h3,.articleCtt .articleTxt .articleSection.articleSection--resumo h3{text-transform:lowercase!important}.articleCtt .articleTxt .articleSection.articleSection--abstract h3::first-letter,.articleCtt .articleTxt .articleSection.articleSection--resumen h3::first-letter,.articleCtt .articleTxt .articleSection.articleSection--resumo h3::first-letter{text-transform:uppercase!important}.articleCtt .articleTxt .paragraph{position:relative;margin-bottom:25px;font-size:1em;line-height:1.7em}.articleCtt .articleTxt .btn.primary{background:#fff;font-size:1.1em;padding:10px 15px}.articleCtt .articleTxt .btn.primary:hover{color:#fff}.articleCtt .articleTxt span.formula{display:block;margin:20px 0;padding:7px;text-align:center;font-size:2em;border-radius:3px}.articleCtt .articleTxt span.formula img{max-width:95%}.articleCtt .articleTxt p{margin:0 0 15px;padding:0;overflow-wrap:break-word;word-wrap:break-word;-ms-word-break:break-all;word-break:break-all;word-break:break-word;-ms-hyphens:auto;-moz-hyphens:auto;-webkit-hyphens:auto}.articleCtt .articleTxt .articleReferral{display:flex;align-items:center;position:relative;margin-bottom:20px;padding:15px 50px 15px 140px;border:1px solid #f3f2e4;vertical-align:middle;min-height:168px}.articleCtt .articleTxt .articleReferral .arText{padding-left:25px}.articleCtt .articleTxt .articleReferral .arText h2{margin-top:0}.articleCtt .articleTxt .articleReferral .arText p{margin-bottom:0}.articleCtt .articleTxt .articleReferral .arPicture{position:relative;width:98px;margin-left:-120px}.articleCtt .articleTxt .articleReferral .arPicture small{font-size:62%;white-space:nowrap}.articleCtt .articleTxt .articleReferral .arPicture small span{display:block}.articleCtt .articleTxt .articleReferral.noPicture{padding:15px 40px}.articleCtt .articleTxt .articleReferral.noPicture .arText{padding-left:0}.articleCtt .articleTxt .articleReferral.biography .arPicture{margin-top:-40px}.articleCtt .articleMenu{position:absolute;margin:25px 0 90px 0;padding:0 15px 0 0;font-size:.85em}.articleCtt .articleMenu.fixed{position:fixed;top:50px}.articleCtt .articleMenu.fixedBottom{position:absolute;top:initial;bottom:50px}.articleCtt .articleMenu li{list-style:none;padding-left:17px}.articleCtt .articleMenu li:before{content:'\00bb';display:inline-block;width:12px;text-align:center;margin-left:-17px;vertical-align:middle;margin-bottom:5px;color:#6c6b6b}.scielo__theme--dark .articleCtt .articleMenu li:before{color:#adadad}.scielo__theme--light .articleCtt .articleMenu li:before{color:#6c6b6b}.articleCtt .articleMenu li.link-to-top{margin-top:20px}.articleCtt .articleMenu li.link-to-top:before{content:'';display:inline;width:auto}.articleCtt .articleMenu li.link-to-top a .circle{width:20px;height:20px;display:inline-block;color:#fff;border-radius:100px;padding:0 0 0 3px;font-size:125%}.articleCtt .articleMenu a{display:inline-block;width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;vertical-align:middle;margin-bottom:5px;text-decoration:none;color:#6c6b6b;text-transform:lowercase!important}.scielo__theme--dark .articleCtt .articleMenu a{color:#adadad}.scielo__theme--light .articleCtt .articleMenu a{color:#6c6b6b}.articleCtt .articleMenu a::first-letter{text-transform:uppercase!important}.articleCtt .articleMenu ul{margin:0;padding:0}.articleCtt .articleMenu li li{padding-left:7px}.articleCtt .articleMenu li li:before{display:none}.articleCtt .articleMenu li.selected:before,.articleCtt .articleMenu li.selected>a{font-weight:700;color:#00314c}.scielo__theme--dark .articleCtt .articleMenu li.selected:before,.scielo__theme--dark .articleCtt .articleMenu li.selected>a{color:#eee}.scielo__theme--light .articleCtt .articleMenu li.selected:before,.scielo__theme--light .articleCtt .articleMenu li.selected>a{color:#00314c}.articleCtt .articleMenu li.selected li a,.articleCtt .articleMenu li.selected li:before{color:#00314c}.scielo__theme--dark .articleCtt .articleMenu li.selected li a,.scielo__theme--dark .articleCtt .articleMenu li.selected li:before{color:#eee}.scielo__theme--light .articleCtt .articleMenu li.selected li a,.scielo__theme--light .articleCtt .articleMenu li.selected li:before{color:#00314c}.articleCtt .articleMenu a:active,.articleCtt .articleMenu a:focus,.articleCtt .articleMenu a:visited{text-decoration:none}.articleCtt .articleFigure{position:absolute;border:4px solid #efeeec}.articleCtt .table-notes a{display:block;padding:5px 0}.articleCtt .nav-tabs li:first-child{margin-left:15px}.articleCtt .nav-tabs a{border:1px solid #ddd;background:#fff}.articleCtt .nav-tabs a:hover{background:#f7f6f4}.scielo__theme--dark .articleCtt .nav-tabs a:hover{background:#393939}.scielo__theme--light .articleCtt .nav-tabs a:hover{background:#f7f6f4}.articleCtt .articleTimeline{margin:0 0 25px;padding:0;font-size:.9em}.articleCtt .articleTimeline li{display:inline-block;width:33%;height:40px;padding-left:25px;list-style:none}.articleCtt .articleTimeline li:before{content:'\00bb';margin-left:-25px;display:inline-block;width:22px;text-align:center}.articleCtt .documentLicense{margin:20px 0}.articleCtt .documentLicense .container-license{font-size:.8em;padding:25px;width:100%;background:#efeeec}.scielo__theme--dark .articleCtt .documentLicense .container-license{background:#414141}.scielo__theme--light .articleCtt .documentLicense .container-license{background:#efeeec}.articleCtt .documentLicense .container-license .row div:first-child{text-align:center}.articleCtt .documentLicense .container-license .row div:last-child{padding-left:0}.articleCtt .documentLicense .container-license a{cursor:pointer;display:inline-block}.articleCtt .documentLicense img{margin:0 auto;width:100%}.articleCtt .journalLicense .row{padding-bottom:25px;font-size:.9em}.articleCtt .share{text-align:center;margin-top:-3px;background:url(../img/dashline.png) bottom left repeat-x;padding-bottom:5px}.articleCtt .collapseBlock{font-size:.9em}.articleCtt .collapseBlock .collapseTitle{position:relative;display:block;background:url(../img/dashline.png) bottom left repeat-x;padding:7px 2px}.articleCtt .collapseBlock .collapseTitle .collapseIcon{position:absolute;right:0}.articleCtt .collapseBlock .collapseTitle:active,.articleCtt .collapseBlock .collapseTitle:focus{text-decoration:none}.articleCtt .collapseContent{background:#f7f6f5;font-size:.9em;padding:15px}.articleCtt .collapseContent ul{margin:0;padding:0}.articleCtt .collapseContent li{list-style:none;padding-left:15px;margin-bottom:5px}.articleCtt .collapseContent li:before{content:'\00bb';display:inline-block;width:13px;text-align:center;margin-left:-15px}.articleCtt .collapseContent .logos:before{width:22px;height:12px;display:inline-block;content:'';background:url(../img/button.glyphs.png) no-repeat}.articleCtt .collapseContent .logos.scielo:before{background-position:center -4556px}.articleCtt .collapseContent .logos.fapesp:before{background-position:center -4506px}.articleCtt .collapseContent .logos.google:before{background-position:center -4531px}.articleCtt .functionsBlock{position:absolute;right:0;z-index:99}.articleCtt .articleBadge{text-align:center}.articleCtt .articleBadge span{display:inline-block;padding:5px 10px;margin:0 0 15px 0;border-radius:4px;position:relative;font-size:20px}.articleCtt .articleBadge span:after{content:'';position:absolute;left:0;right:0;bottom:0}.articleCtt .articleCttLeft .article-title,.articleCtt .articleCttLeft .articleBadge,.articleCtt .articleCttLeft .articleMeta,.articleCtt .articleCttLeft .editionMeta{text-align:left!important}.articleCtt .articleCttLeft .contribGroup{margin-left:0;margin-right:0;text-align:left}.articleCtt .articleCttLeft .contribGroup .dropdown:first-child{margin-left:-10px}#translateArticleModal .modal-body{font-size:.9em;background:#f7f6f4}.scielo__theme--dark #translateArticleModal .modal-body{background:#393939}.scielo__theme--light #translateArticleModal .modal-body{background:#f7f6f4}#translateArticleModal .modal-footer{margin-top:0}#translateArticleModal .dashline{padding-bottom:5px;background:url(../img/dashline.png) bottom left repeat-x}#translateArticleModal th{padding:12px;border:1px solid #ddd;border-radius:4px;background:#fdfcf9}#translateArticleModal table{width:100%}#translateArticleModal td{padding:8px 10px;border-bottom:1px solid #dee5f5;text-align:center}#translateArticleModal th{vertical-align:top}.ModalDefault .tab-pane,.articleCtt .tab-pane{font-size:.9em}.ModalDefault .tab-pane p,.articleCtt .tab-pane p{margin:10px 0}.ModalDefault .tab-pane .center,.articleCtt .tab-pane .center{text-align:center}.ModalDefault .tab-pane label,.articleCtt .tab-pane label{font-weight:400;color:#888}.ModalDefault .tab-pane .big,.articleCtt .tab-pane .big{font-size:2em;font-weight:400}.ModalDefault .tab-pane table td,.ModalDefault .tab-pane table th,.articleCtt .tab-pane table td,.articleCtt .tab-pane table th{padding:7px 10px}.ModalDefault .tab-pane table th,.articleCtt .tab-pane table th{font-weight:400;color:#b7b7b7}.ModalDefault .tab-pane a.midGlyph,.articleCtt .tab-pane a.midGlyph{display:block;text-align:center}.ModalDefault .fig,.ModalDefault .table,.articleCtt .fig,.articleCtt .table{margin-top:10px;margin-bottom:40px;position:relative;width:initial}.ModalDefault .fig .col-md-8,.ModalDefault .table .col-md-8,.articleCtt .fig .col-md-8,.articleCtt .table .col-md-8{padding-top:10px}.ModalDefault .fig strong,.ModalDefault .table strong,.articleCtt .fig strong,.articleCtt .table strong{padding:0}.ModalDefault .fig .thumb,.ModalDefault .fig .thumbOff,.ModalDefault .table .thumb,.ModalDefault .table .thumbOff,.articleCtt .fig .thumb,.articleCtt .fig .thumbOff,.articleCtt .table .thumb,.articleCtt .table .thumbOff{height:160px;background-size:100% auto;text-indent:-5000px;background-color:#e5e4e3;cursor:pointer;border-radius:3px;position:relative;border:4px solid #ccc}.scielo__theme--dark .ModalDefault .fig .thumb,.scielo__theme--dark .ModalDefault .fig .thumbOff,.scielo__theme--dark .ModalDefault .table .thumb,.scielo__theme--dark .ModalDefault .table .thumbOff,.scielo__theme--dark .articleCtt .fig .thumb,.scielo__theme--dark .articleCtt .fig .thumbOff,.scielo__theme--dark .articleCtt .table .thumb,.scielo__theme--dark .articleCtt .table .thumbOff{border:4px solid rgba(255,255,255,.3)}.scielo__theme--light .ModalDefault .fig .thumb,.scielo__theme--light .ModalDefault .fig .thumbOff,.scielo__theme--light .ModalDefault .table .thumb,.scielo__theme--light .ModalDefault .table .thumbOff,.scielo__theme--light .articleCtt .fig .thumb,.scielo__theme--light .articleCtt .fig .thumbOff,.scielo__theme--light .articleCtt .table .thumb,.scielo__theme--light .articleCtt .table .thumbOff{border:4px solid #ccc}.ModalDefault .fig .thumb img,.ModalDefault .fig .thumbOff img,.ModalDefault .table .thumb img,.ModalDefault .table .thumbOff img,.articleCtt .fig .thumb img,.articleCtt .fig .thumbOff img,.articleCtt .table .thumb img,.articleCtt .table .thumbOff img{width:100%}.ModalDefault .fig .thumb .zoom,.ModalDefault .fig .thumbOff .zoom,.ModalDefault .table .thumb .zoom,.ModalDefault .table .thumbOff .zoom,.articleCtt .fig .thumb .zoom,.articleCtt .fig .thumbOff .zoom,.articleCtt .table .thumb .zoom,.articleCtt .table .thumbOff .zoom{position:absolute;bottom:10px;right:10px;border-radius:4px;font-size:24px;text-align:center;width:30px;height:30px;line-height:30px;text-indent:0;background-color:#3867ce;color:#fff}.scielo__theme--dark .ModalDefault .fig .thumb .zoom,.scielo__theme--dark .ModalDefault .fig .thumbOff .zoom,.scielo__theme--dark .ModalDefault .table .thumb .zoom,.scielo__theme--dark .ModalDefault .table .thumbOff .zoom,.scielo__theme--dark .articleCtt .fig .thumb .zoom,.scielo__theme--dark .articleCtt .fig .thumbOff .zoom,.scielo__theme--dark .articleCtt .table .thumb .zoom,.scielo__theme--dark .articleCtt .table .thumbOff .zoom{background-color:#86acff;color:#333}.scielo__theme--light .ModalDefault .fig .thumb .zoom,.scielo__theme--light .ModalDefault .fig .thumbOff .zoom,.scielo__theme--light .ModalDefault .table .thumb .zoom,.scielo__theme--light .ModalDefault .table .thumbOff .zoom,.scielo__theme--light .articleCtt .fig .thumb .zoom,.scielo__theme--light .articleCtt .fig .thumbOff .zoom,.scielo__theme--light .articleCtt .table .thumb .zoom,.scielo__theme--light .articleCtt .table .thumbOff .zoom{background-color:#3867ce;color:#fff}.ModalDefault .fig .thumbOff,.ModalDefault .table .thumbOff,.articleCtt .fig .thumbOff,.articleCtt .table .thumbOff{font-family:'Material Icons Outlined'!important;text-align:center;line-height:140px;font-size:100px;color:#6c6b6b;text-indent:0;overflow:hidden;background:#fff}.scielo__theme--dark .ModalDefault .fig .thumbOff,.scielo__theme--dark .ModalDefault .table .thumbOff,.scielo__theme--dark .articleCtt .fig .thumbOff,.scielo__theme--dark .articleCtt .table .thumbOff{background:#333;color:#adadad}.scielo__theme--light .ModalDefault .fig .thumbOff,.scielo__theme--light .ModalDefault .table .thumbOff,.scielo__theme--light .articleCtt .fig .thumbOff,.scielo__theme--light .articleCtt .table .thumbOff{background:#fff;color:#6c6b6b}.ModalDefault .fig .thumbOff:before,.ModalDefault .table .thumbOff:before,.articleCtt .fig .thumbOff:before,.articleCtt .table .thumbOff:before{content:"table_chart"}.ModalDefault .fig .thumbImg,.ModalDefault .table .thumbImg,.articleCtt .fig .thumbImg,.articleCtt .table .thumbImg{position:relative;overflow:hidden;box-sizing:border-box;height:140px;border:4px solid #ccc;border-radius:3px;background-color:#ccc;cursor:pointer}.scielo__theme--dark .ModalDefault .fig .thumbImg,.scielo__theme--dark .ModalDefault .table .thumbImg,.scielo__theme--dark .articleCtt .fig .thumbImg,.scielo__theme--dark .articleCtt .table .thumbImg{border:4px solid rgba(255,255,255,.3);background-color:rgba(255,255,255,.3)}.scielo__theme--light .ModalDefault .fig .thumbImg,.scielo__theme--light .ModalDefault .table .thumbImg,.scielo__theme--light .articleCtt .fig .thumbImg,.scielo__theme--light .articleCtt .table .thumbImg{border:4px solid #ccc;background-color:#ccc}.ModalDefault .fig .thumbImg img,.ModalDefault .table .thumbImg img,.articleCtt .fig .thumbImg img,.articleCtt .table .thumbImg img{width:100%;height:auto;min-height:131px;display:block}.ModalDefault .fig .thumbImg .zoom,.ModalDefault .table .thumbImg .zoom,.articleCtt .fig .thumbImg .zoom,.articleCtt .table .thumbImg .zoom{position:absolute;bottom:10px;right:10px;width:30px;height:30px;border-radius:4px;padding:5px;display:inline-block;font-size:24px;line-height:50%;background-color:#3867ce;color:#fff}.scielo__theme--dark .ModalDefault .fig .thumbImg .zoom,.scielo__theme--dark .ModalDefault .table .thumbImg .zoom,.scielo__theme--dark .articleCtt .fig .thumbImg .zoom,.scielo__theme--dark .articleCtt .table .thumbImg .zoom{background-color:#86acff;color:#333}.scielo__theme--light .ModalDefault .fig .thumbImg .zoom,.scielo__theme--light .ModalDefault .table .thumbImg .zoom,.scielo__theme--light .articleCtt .fig .thumbImg .zoom,.scielo__theme--light .articleCtt .table .thumbImg .zoom{background-color:#3867ce;color:#fff}.ModalDefault .fig .preview,.ModalDefault .table .preview,.articleCtt .fig .preview,.articleCtt .table .preview{position:absolute;border-radius:3px;border:4px solid #e5e4e3;background-color:#fff;top:0;right:0;z-index:99;padding:10px}.ModalDefault .fig .preview img,.ModalDefault .table .preview img,.articleCtt .fig .preview img,.articleCtt .table .preview img{width:100%}.ModalDefault .fig .figInfo,.ModalDefault .table .figInfo,.articleCtt .fig .figInfo,.articleCtt .table .figInfo{padding:10px 10px 10px 45px;line-height:1.4em;color:#8a8987;position:relative}.ModalDefault .fig .figInfo .glyphBtn,.ModalDefault .table .figInfo .glyphBtn,.articleCtt .fig .figInfo .glyphBtn,.articleCtt .table .figInfo .glyphBtn{position:absolute;top:10px;margin-left:-34px}.ModalDefault .formula,.articleCtt .formula{text-align:center;font-family:"Times New Roman",Times,serif;margin-bottom:15px}.ModalDefault .formula span,.articleCtt .formula span{font-family:Arial;font-weight:700;font-size:16px;display:block;width:100%}.ModalDefault .formula .formula-container,.articleCtt .formula .formula-container{width:100%;display:flex;align-content:center;align-items:center;position:relative;flex-direction:column}.ModalDefault .formula .formula-container .MathJax_Display,.ModalDefault .formula .formula-container .MathJax_SVG,.ModalDefault .formula .formula-container .MathJax_SVG_Display,.ModalDefault .formula .formula-container .formula-body,.articleCtt .formula .formula-container .MathJax_Display,.articleCtt .formula .formula-container .MathJax_SVG,.articleCtt .formula .formula-container .MathJax_SVG_Display,.articleCtt .formula .formula-container .formula-body{flex:99;font-size:1.4rem!important}.ModalDefault .formula .formula-container>span,.articleCtt .formula .formula-container>span{flex:1}.ModalDefault .formula .formula-container .label,.articleCtt .formula .formula-container .label{flex:1;color:#000;font-size:1.4rem;display:block;width:auto}.ModalDefault .formula .formula-container .label:first-child,.articleCtt .formula .formula-container .label:first-child{left:0}.ModalDefault .formula .formula-container .label:last-child,.articleCtt .formula .formula-container .label:last-child{right:0}.ModalDefault .formula svg,.articleCtt .formula svg{display:block;width:100%}.ModalDefault .modal-center{text-align:center}.ModalDefault .md-list{margin:0;padding:0;list-style:none}.ModalDefault .md-list li{margin-bottom:4px}.ModalDefault .md-list li:last-child{margin-bottom:0}.ModalDefault .md-list li.colspan3{margin-bottom:10px}.ModalDefault .md-list li.colspan3 a{display:flex;vertical-align:middle;justify-content:center;align-items:center;height:63px;white-space:normal;overflow:hidden}.ModalDefault .md-list.inline li{float:left;min-width:18%;margin-right:10px}.ModalDefault .md-tabs{margin:0;padding:0}.ModalDefault .md-tabs>li{display:flex;align-items:center;justify-content:center;text-align:center;padding:0}.ModalDefault .md-tabs>li a{padding:5px;display:inline-block;margin:0;border:none;color:#7f7a71}.ModalDefault .md-tabs>li a:focus,.ModalDefault .md-tabs>li a:hover{background:0 0}.ModalDefault .md-tabs>li.active a:focus,.ModalDefault .md-tabs>li.active a:hover{border:none;background:0 0}.ModalDefault .md-tabs>li.active .figureIconGray{background-position:center -4414px}.ModalDefault .md-tabs>li.active .tableIconGray{background-position:center -4368px}.ModalDefault .md-tabs>li .glyphBtn{width:40px;height:40px}.ModalDefault .fig,.ModalDefault .table{margin-top:20px;margin-bottom:20px}.ModalTutors .info{padding:28px 0;border-bottom:1px dotted #ccc}.scielo__theme--dark .ModalTutors .info{border-bottom:1px dotted rgba(255,255,255,.3)}.scielo__theme--light .ModalTutors .info{border-bottom:1px dotted #ccc}.ModalTutors .info:last-child{border-bottom:0}.ModalTutors .info:first-child{padding-top:0}.ModalTutors .info h3{margin:0 0 15px;font-size:1.429em;font-weight:400}.ModalTutors .info .tutors{margin-bottom:25px}.ModalTutors .info .tutors strong:first-child{font-size:1.071em}.ModalTutors .info .tutors:last-child{margin-bottom:0}.ModalTutors ul li{margin-top:10px;border-color:#e0e0df}.ModalTutors ul li.inline li{display:inline}#ModalDownloads strong{display:inline-block;padding:15px 0}#ModalDownloads .glyphBtn{width:40px;height:40px}#ModalDownloads [class^=sci-ico-file]{line-height:40px!important;font-size:40px}#ModalArticles .md-tabs,#ModalMetrics .md-tabs{margin:0 0 25px}#ModalArticles .md-tabs>li,#ModalMetrics .md-tabs>li{min-height:50px}#ModalMetrics .outlineFadeLink{margin:0 0 20px;padding:12px;font-size:15px;display:block;text-align:center}#ModalArticles #how2cite-export{margin:20px 0 2px;background:url(../img/dashline.png) top left repeat-x}#ModalArticles #how2cite-export .col-md-2.col-sm-2{width:20%}#ModalArticles .outlineFadeLink{margin-left:0}#ModalArticles .download{display:block}#ModalArticles #citation-ctt{position:absolute;top:-5000px}#ModalArticles #citationCut{position:absolute;top:-5000px}.ModalFigs .modal-title .sci-ico-fileFigure,.ModalFigs .modal-title .sci-ico-fileTable,.ModalTables .modal-title .sci-ico-fileFigure,.ModalTables .modal-title .sci-ico-fileTable{font-size:24px}.ModalFigs .link-newWindow,.ModalTables .link-newWindow{text-decoration:none}.ModalFigs .link-newWindow:hover,.ModalTables .link-newWindow:hover{opacity:1}.ModalFigs .modal-footer,.ModalTables .modal-footer{margin-top:0;text-align:left;background:#f7f6f4}.scielo__theme--dark .ModalFigs .modal-footer,.scielo__theme--dark .ModalTables .modal-footer{background:#393939}.scielo__theme--light .ModalFigs .modal-footer,.scielo__theme--light .ModalTables .modal-footer{background:#f7f6f4}.ModalFigs .modal-title{width:calc(100% - 70px)}.ModalFigs img{float:none}.ModalFigs .modal-body{padding:1rem}.ModalTables .modal-body{overflow:auto}.ModalTables .modal-body:after{content:'';position:absolute;top:0;bottom:0;left:0;right:0;background:#fff url(../img/list.loading.gif) center center no-repeat}.scielo__theme--dark .ModalTables .modal-body:after{background:#333 url(../img/list.loading.gif) center center no-repeat}.scielo__theme--light .ModalTables .modal-body:after{background:#fff url(../img/list.loading.gif) center center no-repeat}.ModalTables .modal-body.cached:after{display:none}.ModalTables .table{margin-top:0;font-size:14px;position:relative;z-index:1}.ModalTables .table .autoWidth{width:auto}.ModalTables .table .striped{background-color:#f8f8f8}.ModalTables .table .inline-graphic{width:100%}.ModalTables .table-hover .table>tbody>tr:hover>td,.ModalTables .table-hover .table>tbody>tr:hover>th{background-color:#f0f3fb}.ModalTables .table-hover .table>tbody>tr:hover>td.striped,.ModalTables .table-hover .table>tbody>tr:hover>th.striped{background-color:#e8eaf2}.ModalTables .ref-list h2{font-size:14px}.ModalTables .ref-list .refList .xref.big{padding:5px 10px}.ModalTables .refList li{padding-bottom:0}.ModalTables .xref{cursor:text}#ModalRelatedArticles .inline li{min-width:48%}#ModalRelatedArticles .inline li:nth-child(2),#ModalRelatedArticles .inline li:nth-child(4){margin-right:0}#ModalVersionsTranslations .modal-body .md-body-dashVertical{display:inline-block;min-height:150px;background:url(../img/dashline.v.png) top center repeat-y}#ModalVersionsTranslations strong{display:inline-block;padding:15px 0}.ModalDefault .md-list li a.lattes,.ModalDefault .md-list li a.researcherid,.ModalDefault .md-list li a.scopus,.articleCtt .contribGroup .btnContribLinks.lattes,.articleCtt .contribGroup .btnContribLinks.researcherid,.articleCtt .contribGroup .btnContribLinks.scopus{padding-left:30px}.ModalDefault .md-list li a.scopus,.articleCtt .contribGroup .btnContribLinks.scopus{background:url(../img/authorIcon-scopus.png) 10px center no-repeat}.ModalDefault .md-list li a.lattes,.articleCtt .contribGroup .btnContribLinks.lattes{background:url(../img/authorIcon-lattes.png) 10px center no-repeat}.ModalDefault .md-list li a.researcherid,.articleCtt .contribGroup .btnContribLinks.researcherid{background:url(../img/authorIcon-researcherid.png) 10px center no-repeat}.ModalDefault .md-list li a.lattes-matteWhite,.articleCtt .contribGroup .btnContribLinks.lattes-matteWhite{background:url(../img/authorIcon-lattes-matteWhite.png) 10px center no-repeat}.articleCtt .article-title.page-header-title,.articleCtt .only-renditions-available p{text-align:center}.articleCtt .only-renditions-available .jumbotron{background-color:#f6f8fa;margin-top:35px}.levelMenu{background:#f7f6f4}.scielo__theme--dark .levelMenu{background:#393939}.scielo__theme--light .levelMenu{background:#f7f6f4}.levelMenu .btn{min-height:38px}.levelMenu .btn.group{width:auto;padding-left:16px!important;padding-right:16px!important}.levelMenu .btn:hover{color:#fff}.levelMenu .btn:hover [class^=sci-ico-]{color:#fff}.levelMenu .dropdown-menu a.current{position:relative;width:100%;font-weight:700}.levelMenu .dropdown-menu a.current:after{content:"✓";display:inline-block;position:absolute;right:5px}.levelMenu-xs .btn-group.btn-group-nav-mobile{width:100%;display:table}.levelMenu-xs .btn-group.btn-group-nav-mobile>.btn{display:table-cell;width:60%}.levelMenu-xs .btn-group.btn-group-nav-mobile>.btn:first-of-type{width:20%}.levelMenu-xs .btn-group.btn-group-nav-mobile>.btn:last-of-type{width:20%}.levelMenu-xs .btn-group.btn-group-nav-mobile>.btn .sci-ico-socialOther{display:inline-block;width:70%}.levelMenu-xs .btn-group.btn-group-nav-mobile-content{width:100%;display:table;padding:5px 2px}.levelMenu-xs .btn-group.btn-group-nav-mobile-content>.dropdown{display:table-cell;width:33%;padding-right:4px}.levelMenu-xs .btn-group.btn-group-nav-mobile-content>.dropdown:last-child{padding-right:0}.levelMenu-xs .btn-group.btn-group-nav-mobile-content>.dropdown:last-child .btn span{width:auto}.levelMenu-xs .btn-group.btn-group-nav-mobile-content>.dropdown:last-child .btn span:nth-child(2){width:55%}.levelMenu-xs .btn-group.btn-group-nav-mobile-content>.dropdown .btn{width:100%;position:relative}.levelMenu-xs .btn-group.btn-group-nav-mobile-content>.dropdown .btn span{width:90%;display:inline-block}.levelMenu-xs .btn-group.btn-group-nav-mobile-content>.dropdown .btn span:last-child{width:auto}.levelMenu-xs .btn-group.btn-group-nav-mobile-content>.dropdown .btn span.caret{position:absolute;right:10px;top:17px}.ref .xref a{color:#b67f00;pointer-events:none}.scielo__theme--dark .ref .xref a{color:#b67f00}.scielo__theme--light .ref .xref a{color:#b67f00}.ref .xref.xrefblue a{color:#3867ce}.scielo__theme--dark .ref .xref.xrefblue a{color:#86acff}.scielo__theme--light .ref .xref.xrefblue a{color:#3867ce}@media screen and (max-width:575px){.ref{position:static}}.ref .opened{background:#3867ce;color:#fff}@media screen and (max-width:575px){.ref .opened{width:90%;margin-left:5%}}.scielo__theme--dark .ref .opened{background:#86acff;color:#333}.scielo__theme--light .ref .opened{background:#3867ce;color:#fff}.h5,h3,h4,h5:not(.modal-title){margin:25px 0 12px}.modal .info{padding-left:24px}.modal .info div{margin-bottom:16px}.modal .info span{list-style:disc;display:list-item}.xref{color:#3867ce}.scielo__theme--dark .xref{color:#86acff}.scielo__theme--light .xref{color:#3867ce} +/*# sourceMappingURL=article.css.map */ \ No newline at end of file diff --git a/manuscripts/static/css/bootstrap.min.css b/manuscripts/static/css/bootstrap.min.css new file mode 100644 index 0000000..efab8ae --- /dev/null +++ b/manuscripts/static/css/bootstrap.min.css @@ -0,0 +1,10 @@ +@charset "UTF-8";/*! + * Bootstrap v5.0.0-beta1 (https://getbootstrap.com/) + * Copyright 2011-2020 The Bootstrap Authors + * Copyright 2011-2020 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) + */@import url(https://fonts.googleapis.com/css2?family=Arapey&family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap);@import url(https://fonts.googleapis.com/icon?family=Material+Icons&display=swap);@import url(https://fonts.googleapis.com/icon?family=Material+Icons+Outlined);@-webkit-keyframes bounce{0%,100%,20%,50%,80%{-webkit-transform:translateY(0)}40%{-webkit-transform:translateY(30px)}60%{-webkit-transform:translateY(15px)}}@-moz-keyframes bounce{0%,100%,20%,50%,80%{-moz-transform:translateY(0)}40%{-moz-transform:translateY(30px)}60%{-moz-transform:translateY(15px)}}@-ms-keyframes bounce{0%,100%,20%,50%,80%{-ms-transform:translateY(0)}40%{-ms-transform:translateY(30px)}60%{-ms-transform:translateY(15px)}}@-o-keyframes bounce{0%,100%,20%,50%,80%{-o-transform:translateY(0)}40%{-o-transform:translateY(30px)}60%{-o-transform:translateY(15px)}}@keyframes bounce{0%,100%,20%,50%,80%{transform:translateY(0)}40%{transform:translateY(30px)}60%{transform:translateY(15px)}}@-webkit-keyframes slideInUp{from{-webkit-transform:translate3d(0,30%,0);transform:translate3d(0,30%,0);visibility:visible;opacity:0}to{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);opacity:1}}.scielo__shadow-1{box-shadow:0 1px 3px rgba(0,0,0,.12),0 1px 2px rgba(0,0,0,.24)}.scielo__shadow-2{box-shadow:0 3px 6px rgba(0,0,0,.16),0 3px 6px rgba(0,0,0,.23)}.scielo__shadow-3{box-shadow:0 10px 20px rgba(0,0,0,.19),0 6px 6px rgba(0,0,0,.23)}.scielo__shadow-4{box-shadow:0 14px 28px rgba(0,0,0,.25),0 10px 10px rgba(0,0,0,.22)}.scielo__shadow-5{box-shadow:0 19px 38px rgba(0,0,0,.3),0 15px 12px rgba(0,0,0,.22)}:root{--scielo-blue:#3867CE;--scielo-indigo:#6628EE;--scielo-purple:#8D5DF8;--scielo-pink:#C32AA3;--scielo-red:#C63800;--scielo-orange:#FF7E4A;--scielo-yellow:#B67F00;--scielo-green:#2C9D45;--scielo-teal:#30A47F;--scielo-cyan:#2195A9;--scielo-white:#fff;--scielo-gray:#333;--scielo-gray-dark:#00314C;--scielo-primary:#3867CE;--scielo-secondary:#fff;--scielo-success:#2C9D45;--scielo-info:#2195A9;--scielo-warning:#B67F00;--scielo-danger:#C63800;--scielo-light:#F7F6F4;--scielo-dark:#393939;--scielo-font-sans-serif:"Noto Sans",sans-serif;--scielo-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--scielo-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0))}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--scielo-font-sans-serif);font-size:1rem;font-weight:400;line-height:1.5;color:#393939;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{text-decoration:underline dotted;cursor:help;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#3867ce;text-decoration:underline}a:hover{color:#2d52a5}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--scielo-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#c32aa3;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#393939;border-radius:.12 .5rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:rgba(0,0,0,.6);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:rgba(0,0,0,.6)}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid rgba(0,0,0,.3);border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:rgba(0,0,0,.6)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--scielo-gutter-x,.5rem);padding-left:var(--scielo-gutter-x,.5rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:100%}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--scielo-gutter-x:1rem;--scielo-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--scielo-gutter-y) * -1);margin-right:calc(var(--scielo-gutter-x)/ -2);margin-left:calc(var(--scielo-gutter-x)/ -2)}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--scielo-gutter-x)/ 2);padding-left:calc(var(--scielo-gutter-x)/ 2);margin-top:var(--scielo-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333%}.col-2{flex:0 0 auto;width:16.66667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333%}.col-5{flex:0 0 auto;width:41.66667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333%}.col-8{flex:0 0 auto;width:66.66667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333%}.col-11{flex:0 0 auto;width:91.66667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333%}.offset-2{margin-left:16.66667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333%}.offset-5{margin-left:41.66667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333%}.offset-8{margin-left:66.66667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333%}.offset-11{margin-left:91.66667%}.g-0,.gx-0{--scielo-gutter-x:0}.g-0,.gy-0{--scielo-gutter-y:0}.g-1,.gx-1{--scielo-gutter-x:0.25rem}.g-1,.gy-1{--scielo-gutter-y:0.25rem}.g-2,.gx-2{--scielo-gutter-x:0.5rem}.g-2,.gy-2{--scielo-gutter-y:0.5rem}.g-3,.gx-3{--scielo-gutter-x:1rem}.g-3,.gy-3{--scielo-gutter-y:1rem}.g-4,.gx-4{--scielo-gutter-x:1.5rem}.g-4,.gy-4{--scielo-gutter-y:1.5rem}.g-5,.gx-5{--scielo-gutter-x:3rem}.g-5,.gy-5{--scielo-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333%}.col-sm-2{flex:0 0 auto;width:16.66667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333%}.col-sm-5{flex:0 0 auto;width:41.66667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333%}.col-sm-8{flex:0 0 auto;width:66.66667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333%}.col-sm-11{flex:0 0 auto;width:91.66667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333%}.offset-sm-2{margin-left:16.66667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333%}.offset-sm-5{margin-left:41.66667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333%}.offset-sm-8{margin-left:66.66667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333%}.offset-sm-11{margin-left:91.66667%}.g-sm-0,.gx-sm-0{--scielo-gutter-x:0}.g-sm-0,.gy-sm-0{--scielo-gutter-y:0}.g-sm-1,.gx-sm-1{--scielo-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--scielo-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--scielo-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--scielo-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--scielo-gutter-x:1rem}.g-sm-3,.gy-sm-3{--scielo-gutter-y:1rem}.g-sm-4,.gx-sm-4{--scielo-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--scielo-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--scielo-gutter-x:3rem}.g-sm-5,.gy-sm-5{--scielo-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333%}.col-md-2{flex:0 0 auto;width:16.66667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333%}.col-md-5{flex:0 0 auto;width:41.66667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333%}.col-md-8{flex:0 0 auto;width:66.66667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333%}.col-md-11{flex:0 0 auto;width:91.66667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333%}.offset-md-2{margin-left:16.66667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333%}.offset-md-5{margin-left:41.66667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333%}.offset-md-8{margin-left:66.66667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333%}.offset-md-11{margin-left:91.66667%}.g-md-0,.gx-md-0{--scielo-gutter-x:0}.g-md-0,.gy-md-0{--scielo-gutter-y:0}.g-md-1,.gx-md-1{--scielo-gutter-x:0.25rem}.g-md-1,.gy-md-1{--scielo-gutter-y:0.25rem}.g-md-2,.gx-md-2{--scielo-gutter-x:0.5rem}.g-md-2,.gy-md-2{--scielo-gutter-y:0.5rem}.g-md-3,.gx-md-3{--scielo-gutter-x:1rem}.g-md-3,.gy-md-3{--scielo-gutter-y:1rem}.g-md-4,.gx-md-4{--scielo-gutter-x:1.5rem}.g-md-4,.gy-md-4{--scielo-gutter-y:1.5rem}.g-md-5,.gx-md-5{--scielo-gutter-x:3rem}.g-md-5,.gy-md-5{--scielo-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333%}.col-lg-2{flex:0 0 auto;width:16.66667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333%}.col-lg-5{flex:0 0 auto;width:41.66667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333%}.col-lg-8{flex:0 0 auto;width:66.66667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333%}.col-lg-11{flex:0 0 auto;width:91.66667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333%}.offset-lg-2{margin-left:16.66667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333%}.offset-lg-5{margin-left:41.66667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333%}.offset-lg-8{margin-left:66.66667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333%}.offset-lg-11{margin-left:91.66667%}.g-lg-0,.gx-lg-0{--scielo-gutter-x:0}.g-lg-0,.gy-lg-0{--scielo-gutter-y:0}.g-lg-1,.gx-lg-1{--scielo-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--scielo-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--scielo-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--scielo-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--scielo-gutter-x:1rem}.g-lg-3,.gy-lg-3{--scielo-gutter-y:1rem}.g-lg-4,.gx-lg-4{--scielo-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--scielo-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--scielo-gutter-x:3rem}.g-lg-5,.gy-lg-5{--scielo-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333%}.col-xl-2{flex:0 0 auto;width:16.66667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333%}.col-xl-5{flex:0 0 auto;width:41.66667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333%}.col-xl-8{flex:0 0 auto;width:66.66667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333%}.col-xl-11{flex:0 0 auto;width:91.66667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333%}.offset-xl-2{margin-left:16.66667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333%}.offset-xl-5{margin-left:41.66667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333%}.offset-xl-8{margin-left:66.66667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333%}.offset-xl-11{margin-left:91.66667%}.g-xl-0,.gx-xl-0{--scielo-gutter-x:0}.g-xl-0,.gy-xl-0{--scielo-gutter-y:0}.g-xl-1,.gx-xl-1{--scielo-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--scielo-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--scielo-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--scielo-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--scielo-gutter-x:1rem}.g-xl-3,.gy-xl-3{--scielo-gutter-y:1rem}.g-xl-4,.gx-xl-4{--scielo-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--scielo-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--scielo-gutter-x:3rem}.g-xl-5,.gy-xl-5{--scielo-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333%}.col-xxl-2{flex:0 0 auto;width:16.66667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333%}.col-xxl-5{flex:0 0 auto;width:41.66667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333%}.col-xxl-8{flex:0 0 auto;width:66.66667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333%}.col-xxl-11{flex:0 0 auto;width:91.66667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333%}.offset-xxl-2{margin-left:16.66667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333%}.offset-xxl-5{margin-left:41.66667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333%}.offset-xxl-8{margin-left:66.66667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333%}.offset-xxl-11{margin-left:91.66667%}.g-xxl-0,.gx-xxl-0{--scielo-gutter-x:0}.g-xxl-0,.gy-xxl-0{--scielo-gutter-y:0}.g-xxl-1,.gx-xxl-1{--scielo-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--scielo-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--scielo-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--scielo-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--scielo-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--scielo-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--scielo-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--scielo-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--scielo-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--scielo-gutter-y:3rem}}.table{--scielo-table-bg:transparent;--scielo-table-striped-color:#393939;--scielo-table-striped-bg:rgba(0, 0, 0, 0.05);--scielo-table-active-color:#393939;--scielo-table-active-bg:rgba(0, 0, 0, 0.1);--scielo-table-hover-color:#393939;--scielo-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#393939;vertical-align:top;border-color:rgba(0,0,0,.3)}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--scielo-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--scielo-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:last-child)>:last-child>*{border-bottom-color:currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-striped>tbody>tr:nth-of-type(odd){--scielo-table-accent-bg:var(--scielo-table-striped-bg);color:var(--scielo-table-striped-color)}.table-active{--scielo-table-accent-bg:var(--scielo-table-active-bg);color:var(--scielo-table-active-color)}.table-hover>tbody>tr:hover{--scielo-table-accent-bg:var(--scielo-table-hover-bg);color:var(--scielo-table-hover-color)}.table-primary{--scielo-table-bg:#d7e1f5;--scielo-table-striped-bg:#ccd6e9;--scielo-table-striped-color:#000;--scielo-table-active-bg:#c2cbdd;--scielo-table-active-color:#000;--scielo-table-hover-bg:#c7d0e3;--scielo-table-hover-color:#000;color:#000;border-color:#c2cbdd}.table-secondary{--scielo-table-bg:white;--scielo-table-striped-bg:#f2f2f2;--scielo-table-striped-color:#000;--scielo-table-active-bg:#e6e6e6;--scielo-table-active-color:#000;--scielo-table-hover-bg:#ececec;--scielo-table-hover-color:#000;color:#000;border-color:#e6e6e6}.table-success{--scielo-table-bg:#d5ebda;--scielo-table-striped-bg:#cadfcf;--scielo-table-striped-color:#000;--scielo-table-active-bg:#c0d4c4;--scielo-table-active-color:#000;--scielo-table-hover-bg:#c5d9ca;--scielo-table-hover-color:#000;color:#000;border-color:#c0d4c4}.table-info{--scielo-table-bg:#d3eaee;--scielo-table-striped-bg:#c8dee2;--scielo-table-striped-color:#000;--scielo-table-active-bg:#bed3d6;--scielo-table-active-color:#000;--scielo-table-hover-bg:#c3d8dc;--scielo-table-hover-color:#000;color:#000;border-color:#bed3d6}.table-warning{--scielo-table-bg:#f0e5cc;--scielo-table-striped-bg:#e4dac2;--scielo-table-striped-color:#000;--scielo-table-active-bg:#d8ceb8;--scielo-table-active-color:#000;--scielo-table-hover-bg:#ded4bd;--scielo-table-hover-color:#000;color:#000;border-color:#d8ceb8}.table-danger{--scielo-table-bg:#f4d7cc;--scielo-table-striped-bg:#e8ccc2;--scielo-table-striped-color:#000;--scielo-table-active-bg:#dcc2b8;--scielo-table-active-color:#000;--scielo-table-hover-bg:#e2c7bd;--scielo-table-hover-color:#000;color:#000;border-color:#dcc2b8}.table-light{--scielo-table-bg:#F7F6F4;--scielo-table-striped-bg:#ebeae8;--scielo-table-striped-color:#000;--scielo-table-active-bg:#dedddc;--scielo-table-active-color:#000;--scielo-table-hover-bg:#e4e4e2;--scielo-table-hover-color:#000;color:#000;border-color:#dedddc}.table-dark{--scielo-table-bg:#393939;--scielo-table-striped-bg:#434343;--scielo-table-striped-color:#fff;--scielo-table-active-bg:#4d4d4d;--scielo-table-active-color:#fff;--scielo-table-hover-bg:#484848;--scielo-table-hover-color:#fff;color:#fff;border-color:#4d4d4d}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:rgba(0,0,0,.6)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#393939;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.4);appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#393939;background-color:#fff;border-color:#9cb3e7;outline:0;box-shadow:0 0 0 .25rem rgba(56,103,206,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::placeholder{color:rgba(0,0,0,.6);opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#efeeec;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;margin-inline-end:.75rem;color:#393939;background-color:#efeeec;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#e3e2e0}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;margin-inline-end:.75rem;color:#393939;background-color:#efeeec;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#e3e2e0}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#393939;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.12 .5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.5rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{max-width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 1rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#393939;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23414141' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid rgba(0,0,0,.4);border-radius:.25rem;appearance:none}.form-select:focus{border-color:#9cb3e7;outline:0;box-shadow:0 0 0 .25rem rgba(56,103,206,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{color:rgba(0,0,0,.6);background-color:#efeeec}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #393939}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);appearance:none;color-adjust:exact;transition:background-color .15s ease-in-out,background-position .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-check-input{transition:none}}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#9cb3e7;outline:0;box-shadow:0 0 0 .25rem rgba(56,103,206,.25)}.form-check-input:checked{background-color:#3867ce;border-color:#3867ce}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#3867ce;border-color:#3867ce;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%239cb3e7'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(56,103,206,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(56,103,206,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#3867ce;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#c3d1f0}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:rgba(0,0,0,.3);border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#3867ce;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{transition:none}}.form-range::-moz-range-thumb:active{background-color:#c3d1f0}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:rgba(0,0,0,.3);border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:rgba(0,0,0,.5)}.form-range:disabled::-moz-range-thumb{background-color:rgba(0,0,0,.5)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);padding:1rem .75rem}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#393939;text-align:center;white-space:nowrap;background-color:#efeeec;border:1px solid rgba(0,0,0,.4);border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.5rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.12 .5rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:1.75rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#2c9d45}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#000;background-color:rgba(44,157,69,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#2c9d45;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%232C9D45' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#2c9d45;box-shadow:0 0 0 .25rem rgba(44,157,69,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#2c9d45;padding-right:calc(.75em + 2.3125rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23414141' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%232C9D45' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 1.75rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#2c9d45;box-shadow:0 0 0 .25rem rgba(44,157,69,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#2c9d45}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#2c9d45}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(44,157,69,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#2c9d45}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#c63800}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(198,56,0,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#c63800;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23C63800'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23C63800' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#c63800;box-shadow:0 0 0 .25rem rgba(198,56,0,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#c63800;padding-right:calc(.75em + 2.3125rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23414141' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23C63800'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23C63800' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 1.75rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#c63800;box-shadow:0 0 0 .25rem rgba(198,56,0,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#c63800}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#c63800}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(198,56,0,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#c63800}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#393939;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#393939}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(56,103,206,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#3867ce;border-color:#3867ce}.btn-primary:hover{color:#fff;background-color:#3058af;border-color:#2d52a5}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#3058af;border-color:#2d52a5;box-shadow:0 0 0 .25rem rgba(86,126,213,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#2d52a5;border-color:#2a4d9b}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(86,126,213,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#3867ce;border-color:#3867ce}.btn-secondary{color:#000;background-color:#fff;border-color:#fff}.btn-secondary:hover{color:#000;background-color:#fff;border-color:#fff}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#000;background-color:#fff;border-color:#fff;box-shadow:0 0 0 .25rem rgba(217,217,217,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#000;background-color:#fff;border-color:#fff}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,217,217,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#000;background-color:#fff;border-color:#fff}.btn-success{color:#000;background-color:#2c9d45;border-color:#2c9d45}.btn-success:hover{color:#000;background-color:#4cac61;border-color:#41a758}.btn-check:focus+.btn-success,.btn-success:focus{color:#000;background-color:#4cac61;border-color:#41a758;box-shadow:0 0 0 .25rem rgba(37,133,59,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#000;background-color:#56b16a;border-color:#41a758}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(37,133,59,.5)}.btn-success.disabled,.btn-success:disabled{color:#000;background-color:#2c9d45;border-color:#2c9d45}.btn-info{color:#000;background-color:#2195a9;border-color:#2195a9}.btn-info:hover{color:#000;background-color:#42a5b6;border-color:#37a0b2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#42a5b6;border-color:#37a0b2;box-shadow:0 0 0 .25rem rgba(28,127,144,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#4daaba;border-color:#37a0b2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(28,127,144,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#2195a9;border-color:#2195a9}.btn-warning{color:#000;background-color:#b67f00;border-color:#b67f00}.btn-warning:hover{color:#000;background-color:#c19226;border-color:#bd8c1a}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#c19226;border-color:#bd8c1a;box-shadow:0 0 0 .25rem rgba(155,108,0,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#c59933;border-color:#bd8c1a}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(155,108,0,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#b67f00;border-color:#b67f00}.btn-danger{color:#fff;background-color:#c63800;border-color:#c63800}.btn-danger:hover{color:#fff;background-color:#a83000;border-color:#9e2d00}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#a83000;border-color:#9e2d00;box-shadow:0 0 0 .25rem rgba(207,86,38,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#9e2d00;border-color:#952a00}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(207,86,38,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#c63800;border-color:#c63800}.btn-light{color:#000;background-color:#f7f6f4;border-color:#f7f6f4}.btn-light:hover{color:#000;background-color:#f8f7f6;border-color:#f8f7f5}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f8f7f6;border-color:#f8f7f5;box-shadow:0 0 0 .25rem rgba(210,209,207,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9f8f6;border-color:#f8f7f5}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(210,209,207,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f7f6f4;border-color:#f7f6f4}.btn-dark{color:#fff;background-color:#393939;border-color:#393939}.btn-dark:hover{color:#fff;background-color:#303030;border-color:#2e2e2e}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#303030;border-color:#2e2e2e;box-shadow:0 0 0 .25rem rgba(87,87,87,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#2e2e2e;border-color:#2b2b2b}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(87,87,87,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#393939;border-color:#393939}.btn-outline-primary{color:#3867ce;border-color:#3867ce}.btn-outline-primary:hover{color:#fff;background-color:#3867ce;border-color:#3867ce}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(56,103,206,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#3867ce;border-color:#3867ce}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(56,103,206,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#3867ce;background-color:transparent}.btn-outline-secondary{color:#fff;border-color:#fff}.btn-outline-secondary:hover{color:#000;background-color:#fff;border-color:#fff}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(255,255,255,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#000;background-color:#fff;border-color:#fff}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(255,255,255,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#fff;background-color:transparent}.btn-outline-success{color:#2c9d45;border-color:#2c9d45}.btn-outline-success:hover{color:#000;background-color:#2c9d45;border-color:#2c9d45}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(44,157,69,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#000;background-color:#2c9d45;border-color:#2c9d45}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(44,157,69,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#2c9d45;background-color:transparent}.btn-outline-info{color:#2195a9;border-color:#2195a9}.btn-outline-info:hover{color:#000;background-color:#2195a9;border-color:#2195a9}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(33,149,169,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#2195a9;border-color:#2195a9}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(33,149,169,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#2195a9;background-color:transparent}.btn-outline-warning{color:#b67f00;border-color:#b67f00}.btn-outline-warning:hover{color:#000;background-color:#b67f00;border-color:#b67f00}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(182,127,0,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#b67f00;border-color:#b67f00}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(182,127,0,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#b67f00;background-color:transparent}.btn-outline-danger{color:#c63800;border-color:#c63800}.btn-outline-danger:hover{color:#fff;background-color:#c63800;border-color:#c63800}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(198,56,0,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#c63800;border-color:#c63800}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(198,56,0,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#c63800;background-color:transparent}.btn-outline-light{color:#f7f6f4;border-color:#f7f6f4}.btn-outline-light:hover{color:#000;background-color:#f7f6f4;border-color:#f7f6f4}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(247,246,244,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f7f6f4;border-color:#f7f6f4}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(247,246,244,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f7f6f4;background-color:transparent}.btn-outline-dark{color:#393939;border-color:#393939}.btn-outline-dark:hover{color:#fff;background-color:#393939;border-color:#393939}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(57,57,57,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#393939;border-color:#393939}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(57,57,57,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#393939;background-color:transparent}.btn-link{font-weight:400;color:#3867ce;text-decoration:underline}.btn-link:hover{color:#2d52a5}.btn-link.disabled,.btn-link:disabled{color:rgba(0,0,0,.6)}.btn-group-lg>.btn,.btn-group.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.5rem}.btn-group-sm>.btn,.btn-group.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.12 .5rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#393939;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu{top:0;right:auto;left:100%}.dropend .dropdown-menu[data-bs-popper]{margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu{top:0;right:100%;left:auto}.dropstart .dropdown-menu[data-bs-popper]{margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#393939;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#333;background-color:#f7f6f4}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#3867ce}.dropdown-item.disabled,.dropdown-item:disabled{color:rgba(0,0,0,.6);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:rgba(0,0,0,.6);white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#393939}.dropdown-menu-dark{color:rgba(0,0,0,.3);background-color:#414141;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:rgba(0,0,0,.3)}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#3867ce}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:rgba(0,0,0,.5)}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:rgba(0,0,0,.3)}.dropdown-menu-dark .dropdown-header{color:rgba(0,0,0,.5)}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link.disabled{color:rgba(0,0,0,.6);pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid rgba(0,0,0,.3)}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#efeeec #efeeec rgba(0,0,0,.3);isolation:isolate}.nav-tabs .nav-link.disabled{color:rgba(0,0,0,.6);background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:rgba(0,0,0,.7);background-color:#fff;border-color:rgba(0,0,0,.3) rgba(0,0,0,.3) #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#3867ce}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--scielo-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.5rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#393939;text-align:left;background-color:transparent;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#325db9;background-color:#ebf0fa;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23325db9'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23393939'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#9cb3e7;outline:0;box-shadow:0 0 0 .25rem rgba(56,103,206,.25)}.accordion-header{margin-bottom:0}.accordion-item{margin-bottom:-1px;background-color:transparent;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:last-of-type{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:rgba(0,0,0,.6);content:var(--scielo-breadcrumb-divider, "/")}.breadcrumb-item.active{color:rgba(0,0,0,.6)}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#3867ce;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.3);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#2d52a5;background-color:#efeeec;border-color:rgba(0,0,0,.3)}.page-link:focus{z-index:3;color:#2d52a5;background-color:#efeeec;outline:0;box-shadow:0 0 0 .25rem rgba(56,103,206,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#3867ce;border-color:#3867ce}.page-item.disabled .page-link{color:rgba(0,0,0,.6);pointer-events:none;background-color:#fff;border-color:rgba(0,0,0,.3)}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.5rem;border-bottom-left-radius:.5rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.5rem;border-bottom-right-radius:.5rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.12 .5rem;border-bottom-left-radius:.12 .5rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.12 .5rem;border-bottom-right-radius:.12 .5rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#223e7c;background-color:#d7e1f5;border-color:#c3d1f0}.alert-primary .alert-link{color:#1b3263}.alert-secondary{color:#666;background-color:#fff;border-color:#fff}.alert-secondary .alert-link{color:#525252}.alert-success{color:#1a5e29;background-color:#d5ebda;border-color:#c0e2c7}.alert-success .alert-link{color:#154b21}.alert-info{color:#145965;background-color:#d3eaee;border-color:#bcdfe5}.alert-info .alert-link{color:#104751}.alert-warning{color:#6d4c00;background-color:#f0e5cc;border-color:#e9d9b3}.alert-warning .alert-link{color:#573d00}.alert-danger{color:#720;background-color:#f4d7cc;border-color:#eec3b3}.alert-danger .alert-link{color:#5f1b00}.alert-light{color:#636262;background-color:#fdfdfd;border-color:#fdfcfc}.alert-light .alert-link{color:#4f4e4e}.alert-dark{color:#222;background-color:#d7d7d7;border-color:#c4c4c4}.alert-dark .alert-link{color:#1b1b1b}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#efeeec;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#3867ce;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:rgba(0,0,0,.7);text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:rgba(0,0,0,.7);text-decoration:none;background-color:#f7f6f4}.list-group-item-action:active{color:#393939;background-color:#efeeec}.list-group-item{position:relative;display:block;padding:.5rem 1rem;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:rgba(0,0,0,.6);pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#3867ce;border-color:#3867ce}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#223e7c;background-color:#d7e1f5}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#223e7c;background-color:#c2cbdd}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#223e7c;border-color:#223e7c}.list-group-item-secondary{color:#666;background-color:#fff}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#666;background-color:#e6e6e6}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#666;border-color:#666}.list-group-item-success{color:#1a5e29;background-color:#d5ebda}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#1a5e29;background-color:#c0d4c4}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#1a5e29;border-color:#1a5e29}.list-group-item-info{color:#145965;background-color:#d3eaee}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#145965;background-color:#bed3d6}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#145965;border-color:#145965}.list-group-item-warning{color:#6d4c00;background-color:#f0e5cc}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#6d4c00;background-color:#d8ceb8}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#6d4c00;border-color:#6d4c00}.list-group-item-danger{color:#720;background-color:#f4d7cc}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#720;background-color:#dcc2b8}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#720;border-color:#720}.list-group-item-light{color:#636262;background-color:#fdfdfd}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636262;background-color:#e4e4e4}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636262;border-color:#636262}.list-group-item-dark{color:#222;background-color:#d7d7d7}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#222;background-color:#c2c2c2}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#222;border-color:#222}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(56,103,206,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast:not(.showing):not(.show){opacity:0}.toast.hide{display:none}.toast-container{width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.5rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:rgba(0,0,0,.6);background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1060;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.5rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid rgba(0,0,0,.3);border-top-left-radius:calc(.5rem - 1px);border-top-right-radius:calc(.5rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid rgba(0,0,0,.3);border-bottom-right-radius:calc(.5rem - 1px);border-bottom-left-radius:calc(.5rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--scielo-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--scielo-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.5rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid #d8d8d8;border-top-left-radius:calc(.5rem - 1px);border-top-right-radius:calc(.5rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#393939}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{animation-duration:1.5s}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#3867ce}.link-primary:focus,.link-primary:hover{color:#2d52a5}.link-secondary{color:#fff}.link-secondary:focus,.link-secondary:hover{color:#fff}.link-success{color:#2c9d45}.link-success:focus,.link-success:hover{color:#56b16a}.link-info{color:#2195a9}.link-info:focus,.link-info:hover{color:#4daaba}.link-warning{color:#b67f00}.link-warning:focus,.link-warning:hover{color:#c59933}.link-danger{color:#c63800}.link-danger:focus,.link-danger:hover{color:#9e2d00}.link-light{color:#f7f6f4}.link-light:focus,.link-light:hover{color:#f9f8f6}.link-dark{color:#393939}.link-dark:focus,.link-dark:hover{color:#2e2e2e}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--scielo-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--scielo-aspect-ratio:100%}.ratio-4x3{--scielo-aspect-ratio:calc(3 / 4 * 100%)}.ratio-16x9{--scielo-aspect-ratio:calc(9 / 16 * 100%)}.ratio-21x9{--scielo-aspect-ratio:calc(9 / 21 * 100%)}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:sticky;top:0;z-index:1020}}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid rgba(0,0,0,.3)!important}.border-0{border:0!important}.border-top{border-top:1px solid rgba(0,0,0,.3)!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid rgba(0,0,0,.3)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid rgba(0,0,0,.3)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid rgba(0,0,0,.3)!important}.border-start-0{border-left:0!important}.border-primary{border-color:#3867ce!important}.border-secondary{border-color:#fff!important}.border-success{border-color:#2c9d45!important}.border-info{border-color:#2195a9!important}.border-warning{border-color:#b67f00!important}.border-danger{border-color:#c63800!important}.border-light{border-color:#f7f6f4!important}.border-dark{border-color:#393939!important}.border-white{border-color:#fff!important}.border-0{border-width:0!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--scielo-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{color:#3867ce!important}.text-secondary{color:#fff!important}.text-success{color:#2c9d45!important}.text-info{color:#2195a9!important}.text-warning{color:#b67f00!important}.text-danger{color:#c63800!important}.text-light{color:#f7f6f4!important}.text-dark{color:#393939!important}.text-white{color:#fff!important}.text-body{color:#393939!important}.text-muted{color:rgba(0,0,0,.6)!important}.text-black-50{color:rgba(0,0,0,.5)!important}.text-white-50{color:rgba(255,255,255,.5)!important}.text-reset{color:inherit!important}.bg-primary{background-color:#3867ce!important}.bg-secondary{background-color:#fff!important}.bg-success{background-color:#2c9d45!important}.bg-info{background-color:#2195a9!important}.bg-warning{background-color:#b67f00!important}.bg-danger{background-color:#c63800!important}.bg-light{background-color:#f7f6f4!important}.bg-dark{background-color:#393939!important}.bg-body{background-color:#fff!important}.bg-white{background-color:#fff!important}.bg-transparent{background-color:transparent!important}.bg-gradient{background-image:var(--scielo-gradient)!important}.user-select-all{user-select:all!important}.user-select-auto{user-select:auto!important}.user-select-none{user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.12 .5rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.5rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}}.h1,.h2,.h3,.h4,.h5,.h6,body,h1,h2,h3,h4,h5,h6,input,p,textarea{text-rendering:optimizeLegibility}body.top-nav-visible{padding-top:115px}@media (min-width:768px){body.top-nav-visible{padding-top:74px}}.journalInfo{margin-top:74px}::-moz-selection{background:#f8f567}.scielo__theme--dark ::-moz-selection{background:#070a98}.scielo__theme--light ::-moz-selection{background:#f8f567}::selection{background:#f8f567}.scielo__theme--dark ::selection{background:#070a98}.scielo__theme--light ::selection{background:#f8f567}.scielo__theme--light{background:#fff;color:#333}.scielo__theme--dark{background:#333;color:#c4c4c4}.container{padding-left:16px;padding-right:16px}@media screen and (min-width:576px){.col,.container,[class*=col-]{padding-left:10px;padding-right:10px}.row{margin-left:-10px;margin-right:-10px}}@media screen and (min-width:768px){.col,.container,[class*=col-]{padding-left:12px;padding-right:12px}.row{margin-left:-12px;margin-right:-12px}}@media screen and (min-width:992px){.col,.container,[class*=col-]{padding-left:16px;padding-right:16px}.row{margin-left:-16px;margin-right:-16px}}@media screen and (min-width:1200px){.col,.container,[class*=col-]{padding-left:20px;padding-right:20px}.row{margin-left:-20px;margin-right:-20px}}a{color:#3867ce;text-decoration:none;word-wrap:break-word}a:hover{text-decoration:underline}a:hover{color:#254895}.scielo__theme--dark a{color:#86acff}.scielo__theme--dark a:hover{color:#d3e0ff}.scielo__theme--light a{color:#3867ce;text-decoration:none;word-wrap:break-word}.scielo__theme--light a:hover{text-decoration:underline}.scielo__theme--light a:hover{color:#254895}a .material-icons,a .material-icons-outlined{vertical-align:text-bottom}p{line-height:1.6;margin-bottom:1.5rem}p .material-icons,p .material-icons-outlined{vertical-align:text-bottom}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{color:#00314c;margin-bottom:1.5rem}.h1 .material-icons,.h1 .material-icons-outlined,.h2 .material-icons,.h2 .material-icons-outlined,.h3 .material-icons,.h3 .material-icons-outlined,.h4 .material-icons,.h4 .material-icons-outlined,.h5 .material-icons,.h5 .material-icons-outlined,.h6 .material-icons,.h6 .material-icons-outlined,h1 .material-icons,h1 .material-icons-outlined,h2 .material-icons,h2 .material-icons-outlined,h3 .material-icons,h3 .material-icons-outlined,h4 .material-icons,h4 .material-icons-outlined,h5 .material-icons,h5 .material-icons-outlined,h6 .material-icons,h6 .material-icons-outlined{vertical-align:text-bottom;font-size:inherit;line-height:inherit;display:inline-block}.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,h1 .small,h1 small,h2 .small,h2 small,h3 .small,h3 small,h4 .small,h4 small,h5 .small,h5 small,h6 .small,h6 small{color:#333;font-weight:inherit}.scielo__theme--dark .h1,.scielo__theme--dark .h2,.scielo__theme--dark .h3,.scielo__theme--dark .h4,.scielo__theme--dark .h5,.scielo__theme--dark .h6,.scielo__theme--dark h1,.scielo__theme--dark h2,.scielo__theme--dark h3,.scielo__theme--dark h4,.scielo__theme--dark h5,.scielo__theme--dark h6{color:#eee}.scielo__theme--dark .h1 .small,.scielo__theme--dark .h1 small,.scielo__theme--dark .h2 .small,.scielo__theme--dark .h2 small,.scielo__theme--dark .h3 .small,.scielo__theme--dark .h3 small,.scielo__theme--dark .h4 .small,.scielo__theme--dark .h4 small,.scielo__theme--dark .h5 .small,.scielo__theme--dark .h5 small,.scielo__theme--dark .h6 .small,.scielo__theme--dark .h6 small,.scielo__theme--dark h1 .small,.scielo__theme--dark h1 small,.scielo__theme--dark h2 .small,.scielo__theme--dark h2 small,.scielo__theme--dark h3 .small,.scielo__theme--dark h3 small,.scielo__theme--dark h4 .small,.scielo__theme--dark h4 small,.scielo__theme--dark h5 .small,.scielo__theme--dark h5 small,.scielo__theme--dark h6 .small,.scielo__theme--dark h6 small{color:#c4c4c4}.scielo__theme--light .h1,.scielo__theme--light .h2,.scielo__theme--light .h3,.scielo__theme--light .h4,.scielo__theme--light .h5,.scielo__theme--light .h6,.scielo__theme--light h1,.scielo__theme--light h2,.scielo__theme--light h3,.scielo__theme--light h4,.scielo__theme--light h5,.scielo__theme--light h6{color:#00314c}.scielo__theme--light .h1 .small,.scielo__theme--light .h1 small,.scielo__theme--light .h2 .small,.scielo__theme--light .h2 small,.scielo__theme--light .h3 .small,.scielo__theme--light .h3 small,.scielo__theme--light .h4 .small,.scielo__theme--light .h4 small,.scielo__theme--light .h5 .small,.scielo__theme--light .h5 small,.scielo__theme--light .h6 .small,.scielo__theme--light .h6 small,.scielo__theme--light h1 .small,.scielo__theme--light h1 small,.scielo__theme--light h2 .small,.scielo__theme--light h2 small,.scielo__theme--light h3 .small,.scielo__theme--light h3 small,.scielo__theme--light h4 .small,.scielo__theme--light h4 small,.scielo__theme--light h5 .small,.scielo__theme--light h5 small,.scielo__theme--light h6 .small,.scielo__theme--light h6 small{color:#333;font-weight:inherit}.h1,.scielo__text-title--1,h1{font-weight:700;font-size:2.5rem;line-height:1.2em;letter-spacing:-.2px}.h1 .material-icons,.h1 .material-icons-outlined,.scielo__text-title--1 .material-icons,.scielo__text-title--1 .material-icons-outlined,h1 .material-icons,h1 .material-icons-outlined{vertical-align:text-bottom;font-size:inherit;line-height:inherit;display:inline-block}.h2,.scielo__text-title--2,h2{font-weight:700;font-size:2rem;line-height:1.2em;letter-spacing:-.16px}.h2 .material-icons,.h2 .material-icons-outlined,.scielo__text-title--2 .material-icons,.scielo__text-title--2 .material-icons-outlined,h2 .material-icons,h2 .material-icons-outlined{vertical-align:text-bottom;font-size:inherit;line-height:inherit;display:inline-block}.h3,.scielo__text-title--3,h3{font-weight:700;font-size:1.75rem;line-height:1.2em;letter-spacing:-.14px}.h3 .material-icons,.h3 .material-icons-outlined,.scielo__text-title--3 .material-icons,.scielo__text-title--3 .material-icons-outlined,h3 .material-icons,h3 .material-icons-outlined{vertical-align:text-bottom;font-size:inherit;line-height:inherit;display:inline-block}.h4,.scielo__text-title--4,h4{font-weight:700;font-size:1.5rem;line-height:1.2em;letter-spacing:-.12px}.h4 .material-icons,.h4 .material-icons-outlined,.scielo__text-title--4 .material-icons,.scielo__text-title--4 .material-icons-outlined,h4 .material-icons,h4 .material-icons-outlined{vertical-align:text-bottom;font-size:inherit;line-height:inherit;display:inline-block}.h5,.scielo__text-subtitle,h5{font-weight:700;font-size:1.3125rem;line-height:1.25em;letter-spacing:0}.h5 .material-icons,.h5 .material-icons-outlined,.scielo__text-subtitle .material-icons,.scielo__text-subtitle .material-icons-outlined,h5 .material-icons,h5 .material-icons-outlined{vertical-align:text-bottom;font-size:inherit;line-height:inherit;display:inline-block}.h6,.scielo__text-subtitle--small,h6{font-size:1.03125rem;letter-spacing:0}.h6 .material-icons,.h6 .material-icons-outlined,.scielo__text-subtitle--small .material-icons,.scielo__text-subtitle--small .material-icons-outlined,h6 .material-icons,h6 .material-icons-outlined{vertical-align:text-bottom;font-size:inherit;line-height:inherit;display:inline-block}.scielo__text-body{font-weight:400;font-size:1rem;line-height:1.6em;letter-spacing:.1px}.scielo__text-body--large{font-size:1.125rem}.scielo__text-body--small{font-size:.75rem;letter-spacing:.06}.scielo__text-body--micro{font-size:.75rem}.scielo__text-overline{font-weight:700;font-size:.75rem;line-height:1.2em;letter-spacing:.06px}.scielo__text-caption{font-weight:400;font-size:1rem;line-height:1.2em}.scielo__text-caption--large{font-weight:700;font-size:1;line-height:1.2em;letter-spacing:0}.scielo__text-button{font-weight:400!important;font-size:1rem;letter-spacing:.1px!important;white-space:nowrap}.scielo__text-button--large{font-size:1.25rem}.mark,mark{background:rgba(0,176,230,.2)}.scielo__theme--dark .mark,.scielo__theme--dark mark{color:#c4c4c4}.scielo__theme--light .mark,.scielo__theme--light mark{color:#333}code{font-size:1.125rem;color:#00314c;background:rgba(0,176,230,.2)}.scielo__theme--dark code{color:#eee}.scielo__theme--light code{color:#00314c}abbr[data-original-title],abbr[title]{text-decoration:none;border-bottom:1px dotted #333}.scielo__theme--dark abbr[data-original-title],.scielo__theme--dark abbr[title]{border-bottom-color:#c4c4c4}.scielo__theme--light abbr[data-original-title],.scielo__theme--light abbr[title]{border-bottom-color:#333}html{font-size:16px}.articleCtt{font-size:18px}.display-1,.display-2,.display-3,.display-4,.h1,.h2,.h3,.h4,.h5,.h6,.lead{color:#00314c}.display-1 .material-icons,.display-1 .material-icons-outlined,.display-2 .material-icons,.display-2 .material-icons-outlined,.display-3 .material-icons,.display-3 .material-icons-outlined,.display-4 .material-icons,.display-4 .material-icons-outlined,.h1 .material-icons,.h1 .material-icons-outlined,.h2 .material-icons,.h2 .material-icons-outlined,.h3 .material-icons,.h3 .material-icons-outlined,.h4 .material-icons,.h4 .material-icons-outlined,.h5 .material-icons,.h5 .material-icons-outlined,.h6 .material-icons,.h6 .material-icons-outlined,.lead .material-icons,.lead .material-icons-outlined{vertical-align:text-bottom;font-size:inherit;line-height:inherit;display:inline-block}.display-1 .small,.display-1 small,.display-2 .small,.display-2 small,.display-3 .small,.display-3 small,.display-4 .small,.display-4 small,.h1 .small,.h1 small,.h2 .small,.h2 small,.h3 .small,.h3 small,.h4 .small,.h4 small,.h5 .small,.h5 small,.h6 .small,.h6 small,.lead .small,.lead small{color:#333;font-weight:inherit}.scielo__theme--dark .display-1,.scielo__theme--dark .display-2,.scielo__theme--dark .display-3,.scielo__theme--dark .display-4,.scielo__theme--dark .h1,.scielo__theme--dark .h2,.scielo__theme--dark .h3,.scielo__theme--dark .h4,.scielo__theme--dark .h5,.scielo__theme--dark .h6,.scielo__theme--dark .lead{color:#eee}.scielo__theme--dark .display-1 .small,.scielo__theme--dark .display-1 small,.scielo__theme--dark .display-2 .small,.scielo__theme--dark .display-2 small,.scielo__theme--dark .display-3 .small,.scielo__theme--dark .display-3 small,.scielo__theme--dark .display-4 .small,.scielo__theme--dark .display-4 small,.scielo__theme--dark .h1 .small,.scielo__theme--dark .h1 small,.scielo__theme--dark .h2 .small,.scielo__theme--dark .h2 small,.scielo__theme--dark .h3 .small,.scielo__theme--dark .h3 small,.scielo__theme--dark .h4 .small,.scielo__theme--dark .h4 small,.scielo__theme--dark .h5 .small,.scielo__theme--dark .h5 small,.scielo__theme--dark .h6 .small,.scielo__theme--dark .h6 small,.scielo__theme--dark .lead .small,.scielo__theme--dark .lead small{color:#c4c4c4}.scielo__theme--light .display-1,.scielo__theme--light .display-2,.scielo__theme--light .display-3,.scielo__theme--light .display-4,.scielo__theme--light .h1,.scielo__theme--light .h2,.scielo__theme--light .h3,.scielo__theme--light .h4,.scielo__theme--light .h5,.scielo__theme--light .h6,.scielo__theme--light .lead{color:#00314c}.scielo__theme--light .display-1 .small,.scielo__theme--light .display-1 small,.scielo__theme--light .display-2 .small,.scielo__theme--light .display-2 small,.scielo__theme--light .display-3 .small,.scielo__theme--light .display-3 small,.scielo__theme--light .display-4 .small,.scielo__theme--light .display-4 small,.scielo__theme--light .h1 .small,.scielo__theme--light .h1 small,.scielo__theme--light .h2 .small,.scielo__theme--light .h2 small,.scielo__theme--light .h3 .small,.scielo__theme--light .h3 small,.scielo__theme--light .h4 .small,.scielo__theme--light .h4 small,.scielo__theme--light .h5 .small,.scielo__theme--light .h5 small,.scielo__theme--light .h6 .small,.scielo__theme--light .h6 small,.scielo__theme--light .lead .small,.scielo__theme--light .lead small{color:#333}.display-1,.h1{font-weight:700;font-size:2.5rem;line-height:1.2em;letter-spacing:-.2px}.display-1 .material-icons,.display-1 .material-icons-outlined,.h1 .material-icons,.h1 .material-icons-outlined{vertical-align:text-bottom;font-size:inherit;line-height:inherit;display:inline-block}.display-2,.h2{font-weight:700;font-size:2rem;line-height:1.2em;letter-spacing:-.16px}.display-2 .material-icons,.display-2 .material-icons-outlined,.h2 .material-icons,.h2 .material-icons-outlined{vertical-align:text-bottom;font-size:inherit;line-height:inherit;display:inline-block}.display-3,.h3{font-weight:700;font-size:1.75rem;line-height:1.2em;letter-spacing:-.14px}.display-3 .material-icons,.display-3 .material-icons-outlined,.h3 .material-icons,.h3 .material-icons-outlined{vertical-align:text-bottom;font-size:inherit;line-height:inherit;display:inline-block}.display-4,.h4{font-weight:700;font-size:1.5rem;line-height:1.2em;letter-spacing:-.12px}.display-4 .material-icons,.display-4 .material-icons-outlined,.h4 .material-icons,.h4 .material-icons-outlined{vertical-align:text-bottom;font-size:inherit;line-height:inherit;display:inline-block}.h5,.lead{font-weight:700;font-size:1.3125rem;line-height:1.25em;letter-spacing:0}.h5 .material-icons,.h5 .material-icons-outlined,.lead .material-icons,.lead .material-icons-outlined{vertical-align:text-bottom;font-size:inherit;line-height:inherit;display:inline-block}.h6{font-size:1.03125rem;letter-spacing:0}.h6 .material-icons,.h6 .material-icons-outlined{vertical-align:text-bottom;font-size:inherit;line-height:inherit;display:inline-block}.blockquote,blockquote{margin:0 0 1rem;padding:.8rem 1.4rem;border-left:4px solid rgba(0,0,0,.3);background-color:#efeeec}.scielo__theme--dark .blockquote,.scielo__theme--dark blockquote{border-left-color:#414141}.scielo__theme--light .blockquote,.scielo__theme--light blockquote{border-left-color:#efeeec}cite{display:block;font-size:.86667rem}cite:before{content:"— "}.form-text,.text-muted{color:#6c6b6b!important}.scielo__theme--dark .form-text,.scielo__theme--dark .text-muted{color:#adadad!important}.scielo__theme--light .form-text,.scielo__theme--light .text-muted{color:#6c6b6b!important}@media (min-width:768px){.scielo__truncate{display:block;max-width:285px}}@font-face{font-family:scielo-social-network;src:url(../fonts/scielo-social-network.eot?dhp6e8);src:url(../fonts/scielo-social-network.eot?dhp6e8#iefix) format("embedded-opentype"),url(../fonts/scielo-social-network.ttf?dhp6e8) format("truetype"),url(../fonts/scielo-social-network.woff?dhp6e8) format("woff"),url(../fonts/scielo-social-network.svg?dhp6e8#scielo-social-network) format("svg");font-weight:400;font-style:normal;font-display:block}[class*=" icon-"],[class^=icon-]{font-family:scielo-social-network!important;speak:never;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.icon-bluesky:before{content:"\e900";color:#616060}.icon-mastodon:before{content:"\e901";color:#616060}.icon-linkedin:before{content:"\e902";color:#616060}.icon-facebook:before{content:"\e903";color:#616060}.icon-youtube:before{content:"\e904";color:#616060}.scielo__social-network-links a{text-decoration:none!important}.scielo__social-network-links a:hover [class*=" icon-"]::before,.scielo__social-network-links a:hover [class^=icon-]::before{color:#3867ce}.logo-open-access{height:2em;width:auto}.h1 .logo-open-access,.h2 .logo-open-access,.h3 .logo-open-access,.h4 .logo-open-access,.h5 .logo-open-access,.h6 .logo-open-access,h1 .logo-open-access,h2 .logo-open-access,h3 .logo-open-access,h4 .logo-open-access,h5 .logo-open-access,h6 .logo-open-access{height:.9em;width:auto}footer .logo-open-access{height:1.5em;width:auto}.scielo__logo-scielo{background-image:url(../img/logo-scielo-no-label.svg);background-size:contain;background-repeat:no-repeat;display:inline-block;width:4rem;height:4rem}.scielo__theme--dark .scielo__logo-scielo{background-image:url(../img/logo-scielo-no-label-negative.svg)}.scielo__theme--light .scielo__logo-scielo{background-image:url(../img/logo-scielo-no-label.svg)}.scielo__logo-scielo--large{background-image:url(../img/logo-scielo-no-label.svg);background-size:contain;background-repeat:no-repeat;display:inline-block;width:15.625rem;height:15.625rem}.scielo__theme--dark .scielo__logo-scielo--large{background-image:url(../img/logo-scielo-no-label-negative.svg)}.scielo__theme--light .scielo__logo-scielo--large{background-image:url(../img/logo-scielo-no-label.svg)}.scielo__logo-scielo--medium{background-image:url(../img/logo-scielo-no-label.svg);background-size:contain;background-repeat:no-repeat;display:inline-block;width:9.375rem;height:9.375rem}.scielo__theme--dark .scielo__logo-scielo--medium{background-image:url(../img/logo-scielo-no-label-negative.svg)}.scielo__theme--light .scielo__logo-scielo--medium{background-image:url(../img/logo-scielo-no-label.svg)}.scielo__logo-scielo--medium.scielo__logo-scielo--caption{margin-bottom:2rem}.scielo__logo-scielo--medium.scielo__logo-scielo--caption .small,.scielo__logo-scielo--medium.scielo__logo-scielo--caption small{position:relative;font-size:1.875rem;font-family:Arapey,serif;color:#333;font-style:italic;padding-left:60px;border-bottom:1px solid #ccc;padding-bottom:12px;padding-right:5px;top:80px;left:110px}.scielo__theme--dark .scielo__logo-scielo--medium.scielo__logo-scielo--caption .small,.scielo__theme--dark .scielo__logo-scielo--medium.scielo__logo-scielo--caption small{color:#c4c4c4;border-color:rgba(255,255,255,.3)}.scielo__theme--light .scielo__logo-scielo--medium.scielo__logo-scielo--caption .small,.scielo__theme--light .scielo__logo-scielo--medium.scielo__logo-scielo--caption small{color:#333;border-color:#ccc}.scielo__logo-scielo--medium.scielo__logo-scielo--caption span{position:relative;left:50%;transform:translateX(-7.1875rem);top:6.25rem;display:block;font-size:1.5rem;font-family:Arapey,serif;color:#333;white-space:nowrap;width:14.375rem}.scielo__theme--dark .scielo__logo-scielo--medium.scielo__logo-scielo--caption span{color:#c4c4c4}.scielo__theme--light .scielo__logo-scielo--medium.scielo__logo-scielo--caption span{color:#333}.scielo__logo-scielo--small{background-image:url(../img/logo-scielo-no-label.svg);background-size:contain;background-repeat:no-repeat;display:inline-block;width:4rem;height:4rem}.scielo__theme--dark .scielo__logo-scielo--small{background-image:url(../img/logo-scielo-no-label-negative.svg)}.scielo__theme--light .scielo__logo-scielo--small{background-image:url(../img/logo-scielo-no-label.svg)}.scielo__logo-scielo--small.scielo__logo-scielo--caption strong{position:relative;left:4.5rem;top:1.75rem;color:#333}.scielo__theme--dark .scielo__logo-scielo--small.scielo__logo-scielo--caption strong{color:#c4c4c4}.scielo__theme--light .scielo__logo-scielo--small.scielo__logo-scielo--caption strong{color:#333}.scielo__logo-scielo-caption{position:relative;width:16.875rem;height:6.25rem;background-image:url(../img/logo-scielo-no-label.svg);background-repeat:no-repeat;background-size:75px auto;background-position-x:calc(50% - 22px);display:inline-block}@media (min-width:576px){.scielo__logo-scielo-caption{width:21.875rem;height:11.375rem;background-size:150px auto;background-position-x:calc(50% - 55px)}}.scielo__theme--dark .scielo__logo-scielo-caption{background-image:url(../img/logo-scielo-no-label-negative.svg)}.scielo__theme--light .scielo__logo-scielo-caption{background-image:url(../img/logo-scielo-no-label.svg)}.scielo__logo-scielo-caption:after{content:"Scientific Electronic Library Online";position:absolute;top:4.375rem;display:block;font-size:1.2rem;font-family:Arapey,serif;color:#333;white-space:nowrap;width:100%;text-align:center}@media (min-width:576px){.scielo__logo-scielo-caption:after{top:9.375rem;font-size:1.5rem}}.scielo__theme--dark .scielo__logo-scielo-caption:after{color:#c4c4c4}.scielo__theme--light .scielo__logo-scielo-caption:after{color:#333}.scielo__logo-scielo-caption .small,.scielo__logo-scielo-caption small{position:absolute;font-size:1rem;font-family:Arapey,serif;color:#333;font-style:italic;border-bottom:1px solid #ccc;padding-bottom:10px;padding-left:24px;padding-right:0;top:40px;left:8.125rem;line-height:1rem}@media (min-width:576px){.scielo__logo-scielo-caption .small,.scielo__logo-scielo-caption small{font-size:1.875rem;padding-bottom:12px;padding-left:60px;padding-right:5px;top:80px;left:9.375rem;line-height:normal}}.scielo__theme--dark .scielo__logo-scielo-caption .small,.scielo__theme--dark .scielo__logo-scielo-caption small{color:#c4c4c4;border-color:rgba(255,255,255,.3)}.scielo__theme--light .scielo__logo-scielo-caption .small,.scielo__theme--light .scielo__logo-scielo-caption small{color:#333;border-color:#ccc}.scielo__logo-scielo-collection{position:relative;background-image:url(../img/logo-scielo-no-label.svg);background-size:contain;background-repeat:no-repeat;display:inline-block;width:4rem;height:4rem}.scielo__theme--dark .scielo__logo-scielo-collection{background-image:url(../img/logo-scielo-no-label-negative.svg)}.scielo__theme--light .scielo__logo-scielo-collection{background-image:url(../img/logo-scielo-no-label.svg)}.scielo__logo-scielo-collection .small,.scielo__logo-scielo-collection small{position:absolute;left:4.5rem;top:1.75rem;color:#333;font-weight:bolder}.scielo__theme--dark .scielo__logo-scielo-collection .small,.scielo__theme--dark .scielo__logo-scielo-collection small{color:#c4c4c4}.scielo__theme--light .scielo__logo-scielo-collection .small,.scielo__theme--light .scielo__logo-scielo-collection small{color:#333}footer .scielo__logo-scielo,header .scielo__logo-scielo{width:64px;height:64px}.scielo__logo-periodico{max-height:100px}.btn{position:relative;display:inline-block;padding:.625rem 1rem;border-radius:.25rem;line-height:1.25rem;height:2.5rem;font-weight:400!important;font-size:1rem;letter-spacing:.1px!important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;background-position:center;transition:all .8s;margin:0 0 1rem;background-color:#fff;border:1px solid #ccc;color:#333}.btn:focus{box-shadow:0 0 0 .125rem rgba(56,103,206,.25);outline:0}.btn:focus:active{box-shadow:0 0 0 .25rem rgba(56,103,206,.25)}.btn:focus{box-shadow:0 0 0 .125rem rgba(204,204,204,.25);outline:0}.btn:focus-visible{outline:3px solid #3867ce;outline-offset:2px}.btn:active{box-shadow:0 0 0 .25rem rgba(204,204,204,.25)}.btn:focus{background-color:#fff;color:#333}.btn:focus-visible{outline:3px solid #3867ce;outline-offset:2px}.btn.active:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background-color:#d9d9d9;color:#333}.show>.btn.dropdown-toggle{background-color:#d9d9d9;color:#333}.btn:hover:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background:#d9d9d9 radial-gradient(circle,transparent 1%,#d9d9d9 1%) center/15000%;color:#333;text-decoration:none}.btn:active:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background-color:#1a1a1a;background-size:100%;transition:background 0s;color:#333}.btn:hover:not(:disabled):not(.disabled){border:1px solid #ccc;background:hoverBgColor radial-gradient(circle,transparent 1%,hoverBgColor 1%) center/15000%;color:#333}.btn:active:not(:disabled):not(.disabled){border:1px solid #ccc;background-color:gray;background-size:100%;transition:background 0s;color:#333}.btn:focus{border-color:#ccc}.scielo__theme--dark .btn{background-color:#c4c4c4;border:1px solid rgba(255,255,255,.3);color:#333}.scielo__theme--dark .btn:focus{background-color:#c4c4c4;color:#333}.scielo__theme--dark .btn.active:not(:disabled):not(.disabled){background-color:#a7a7a7;color:#333}.scielo__theme--dark .btn:hover:not(:disabled):not(.disabled){border:1px solid #dcdcdc;background:#dcdcdc radial-gradient(circle,transparent 1%,#dcdcdc 1%) center/15000%;color:#333;text-decoration:none}.scielo__theme--dark .btn:active:not(:disabled):not(.disabled){border:1px solid #dcdcdc;background-color:#f9f9f9;background-size:100%;transition:background 0s;color:#333}.scielo__theme--dark .btn:hover:not(:disabled):not(.disabled){border:1px solid rgba(255,255,255,.3);background:hoverBgColor radial-gradient(circle,transparent 1%,hoverBgColor 1%) center/15000%;color:#333}.scielo__theme--dark .btn:active:not(:disabled):not(.disabled){border:1px solid rgba(255,255,255,.3);background-color:#f9f9f9;background-size:100%;transition:background 0s;color:#333}.scielo__theme--dark .btn:focus{border-color:rgba(255,255,255,.3)}.scielo__theme--light .btn{background-color:#fff;border-color:1px solid #ccc;color:#333}.scielo__theme--light .btn:focus{background-color:#fff;color:#333}.scielo__theme--light .btn.active:not(:disabled):not(.disabled){background-color:#fff;color:#333}.scielo__theme--light .btn:hover:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background:#d9d9d9 radial-gradient(circle,transparent 1%,#d9d9d9 1%) center/15000%;color:#333;text-decoration:none}.scielo__theme--light .btn:active:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background-color:#1a1a1a;background-size:100%;transition:background 0s;color:#333}.scielo__theme--light .btn:hover:not(:disabled):not(.disabled){border:1px solid #ccc;background:hoverBgColor radial-gradient(circle,transparent 1%,hoverBgColor 1%) center/15000%;color:#333}.scielo__theme--light .btn:active:not(:disabled):not(.disabled){border:1px solid #ccc;background-color:gray;background-size:100%;transition:background 0s;color:#333}.scielo__theme--light .btn:focus{border-color:#ccc}.btn-light,.btn-primary{background-color:#3867ce;border:1px solid #3867ce;color:#fff}.btn-light:focus,.btn-primary:focus{box-shadow:0 0 0 .125rem rgba(56,103,206,.25);outline:0}.btn-light:focus-visible,.btn-primary:focus-visible{outline:3px solid #3867ce;outline-offset:2px}.btn-light:focus:active,.btn-primary:focus:active{box-shadow:0 0 0 .25rem rgba(56,103,206,.25)}.btn-light:focus,.btn-primary:focus{background-color:#3867ce;color:#fff}.btn-light:focus-visible,.btn-primary:focus-visible{outline:3px solid #3867ce;outline-offset:2px}.btn-light.active:not(:disabled):not(.disabled),.btn-primary.active:not(:disabled):not(.disabled){border:1px solid #3058af;background-color:#3058af;color:#fff}.show>.btn-light.dropdown-toggle,.show>.btn-primary.dropdown-toggle{background-color:#3058af;color:#fff}.btn-light:hover:not(:disabled):not(.disabled),.btn-primary:hover:not(:disabled):not(.disabled){border:1px solid #3058af;background:#3058af radial-gradient(circle,transparent 1%,#3058af 1%) center/15000%;color:#fff;text-decoration:none}.btn-light:active:not(:disabled):not(.disabled),.btn-primary:active:not(:disabled):not(.disabled){border:1px solid #3058af;background-color:#060a15;background-size:100%;transition:background 0s;color:#fff}.scielo__theme--dark .btn-light,.scielo__theme--dark .btn-primary{background-color:#86acff;border:1px solid #86acff;color:#333}.scielo__theme--dark .btn-light:focus,.scielo__theme--dark .btn-primary:focus{background-color:#86acff;color:#333}.scielo__theme--dark .btn-light.active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-primary.active:not(:disabled):not(.disabled){background-color:#7292d9;color:#333}.scielo__theme--dark .btn-light:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn-primary:hover:not(:disabled):not(.disabled){border:1px solid #b6cdff;background:#b6cdff radial-gradient(circle,transparent 1%,#b6cdff 1%) center/15000%;color:#333;text-decoration:none}.scielo__theme--dark .btn-light:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-primary:active:not(:disabled):not(.disabled){border:1px solid #b6cdff;background-color:#f3f7ff;background-size:100%;transition:background 0s;color:#333}.scielo__theme--light .btn-light,.scielo__theme--light .btn-primary{background-color:#3867ce;border-color:1px solid #3867ce;color:#fff}.scielo__theme--light .btn-light:focus,.scielo__theme--light .btn-primary:focus{background-color:#3867ce;color:#fff}.scielo__theme--light .btn-light.active:not(:disabled):not(.disabled),.scielo__theme--light .btn-primary.active:not(:disabled):not(.disabled){background-color:#567ed5;color:#fff}.scielo__theme--light .btn-light:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn-primary:hover:not(:disabled):not(.disabled){border:1px solid #3058af;background:#3058af radial-gradient(circle,transparent 1%,#3058af 1%) center/15000%;color:#fff;text-decoration:none}.scielo__theme--light .btn-light:active:not(:disabled):not(.disabled),.scielo__theme--light .btn-primary:active:not(:disabled):not(.disabled){border:1px solid #3058af;background-color:#060a15;background-size:100%;transition:background 0s;color:#fff}.btn-info{background-color:#2195a9;border:1px solid #2195a9;color:#fff}.btn-info:focus{box-shadow:0 0 0 .125rem rgba(33,149,169,.25);outline:0}.btn-info:focus-visible{outline:3px solid #3867ce;outline-offset:2px}.btn-info:focus:active{box-shadow:0 0 0 .25rem rgba(33,149,169,.25)}.btn-info:focus{background-color:#2195a9;color:#fff}.btn-info:focus-visible{outline:3px solid #3867ce;outline-offset:2px}.btn-info.active:not(:disabled):not(.disabled){border:1px solid #1c7f90;background-color:#1c7f90;color:#fff}.show>.btn-info.dropdown-toggle{background-color:#1c7f90;color:#fff}.btn-info:hover:not(:disabled):not(.disabled){border:1px solid #1c7f90;background:#1c7f90 radial-gradient(circle,transparent 1%,#1c7f90 1%) center/15000%;color:#fff;text-decoration:none}.btn-info:active:not(:disabled):not(.disabled){border:1px solid #1c7f90;background-color:#030f11;background-size:100%;transition:background 0s;color:#fff}.scielo__theme--dark .btn-info{background-color:#2299ad;border:1px solid #2299ad;color:#333}.scielo__theme--dark .btn-info:focus{background-color:#2299ad;color:#333}.scielo__theme--dark .btn-info.active:not(:disabled):not(.disabled){background-color:#1d8293;color:#333}.scielo__theme--dark .btn-info:hover:not(:disabled):not(.disabled){border:1px solid #7ac2ce;background:#7ac2ce radial-gradient(circle,transparent 1%,#7ac2ce 1%) center/15000%;color:#333;text-decoration:none}.scielo__theme--dark .btn-info:active:not(:disabled):not(.disabled){border:1px solid #7ac2ce;background-color:#e9f5f7;background-size:100%;transition:background 0s;color:#333}.scielo__theme--light .btn-info{background-color:#2195a9;border-color:1px solid #2195a9;color:#fff}.scielo__theme--light .btn-info:focus{background-color:#2195a9;color:#fff}.scielo__theme--light .btn-info.active:not(:disabled):not(.disabled){background-color:#42a5b6;color:#fff}.scielo__theme--light .btn-info:hover:not(:disabled):not(.disabled){border:1px solid #1c7f90;background:#1c7f90 radial-gradient(circle,transparent 1%,#1c7f90 1%) center/15000%;color:#fff;text-decoration:none}.scielo__theme--light .btn-info:active:not(:disabled):not(.disabled){border:1px solid #1c7f90;background-color:#030f11;background-size:100%;transition:background 0s;color:#fff}.btn-dark{background-color:#fff;border:1px solid #ccc;color:#333}.btn-dark:focus{box-shadow:0 0 0 .125rem rgba(204,204,204,.25);outline:0}.btn-dark:focus-visible{outline:3px solid #3867ce;outline-offset:2px}.btn-dark:active{box-shadow:0 0 0 .25rem rgba(204,204,204,.25)}.btn-dark:focus{background-color:#fff;color:#333}.btn-dark:focus-visible{outline:3px solid #3867ce;outline-offset:2px}.btn-dark.active:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background-color:#d9d9d9;color:#333}.show>.btn-dark.dropdown-toggle{background-color:#d9d9d9;color:#333}.btn-dark:hover:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background:#d9d9d9 radial-gradient(circle,transparent 1%,#d9d9d9 1%) center/15000%;color:#333;text-decoration:none}.btn-dark:active:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background-color:#1a1a1a;background-size:100%;transition:background 0s;color:#333}.btn-dark:hover:not(:disabled):not(.disabled){border:1px solid #ccc;background:hoverBgColor radial-gradient(circle,transparent 1%,hoverBgColor 1%) center/15000%;color:#333}.btn-dark:active:not(:disabled):not(.disabled){border:1px solid #ccc;background-color:gray;background-size:100%;transition:background 0s;color:#333}.btn-dark:focus{border-color:#ccc}.scielo__theme--dark .btn-dark{background-color:#c4c4c4;border:1px solid rgba(255,255,255,.3);color:#333}.scielo__theme--dark .btn-dark:focus{background-color:#c4c4c4;color:#333}.scielo__theme--dark .btn-dark.active:not(:disabled):not(.disabled){background-color:#a7a7a7;color:#333}.scielo__theme--dark .btn-dark:hover:not(:disabled):not(.disabled){border:1px solid #dcdcdc;background:#dcdcdc radial-gradient(circle,transparent 1%,#dcdcdc 1%) center/15000%;color:#333;text-decoration:none}.scielo__theme--dark .btn-dark:active:not(:disabled):not(.disabled){border:1px solid #dcdcdc;background-color:#f9f9f9;background-size:100%;transition:background 0s;color:#333}.scielo__theme--dark .btn-dark:hover:not(:disabled):not(.disabled){border:1px solid rgba(255,255,255,.3);background:hoverBgColor radial-gradient(circle,transparent 1%,hoverBgColor 1%) center/15000%;color:#333}.scielo__theme--dark .btn-dark:active:not(:disabled):not(.disabled){border:1px solid rgba(255,255,255,.3);background-color:#f9f9f9;background-size:100%;transition:background 0s;color:#333}.scielo__theme--dark .btn-dark:focus{border-color:rgba(255,255,255,.3)}.scielo__theme--light .btn-dark{background-color:#fff;border-color:1px solid #ccc;color:#333}.scielo__theme--light .btn-dark:focus{background-color:#fff;color:#333}.scielo__theme--light .btn-dark.active:not(:disabled):not(.disabled){background-color:#fff;color:#333}.scielo__theme--light .btn-dark:hover:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background:#d9d9d9 radial-gradient(circle,transparent 1%,#d9d9d9 1%) center/15000%;color:#333;text-decoration:none}.scielo__theme--light .btn-dark:active:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background-color:#1a1a1a;background-size:100%;transition:background 0s;color:#333}.scielo__theme--light .btn-dark:hover:not(:disabled):not(.disabled){border:1px solid #ccc;background:hoverBgColor radial-gradient(circle,transparent 1%,hoverBgColor 1%) center/15000%;color:#333}.scielo__theme--light .btn-dark:active:not(:disabled):not(.disabled){border:1px solid #ccc;background-color:gray;background-size:100%;transition:background 0s;color:#333}.scielo__theme--light .btn-dark:focus{border-color:#ccc}.btn-success{background-color:#2c9d45;border:1px solid #2c9d45;color:#fff}.btn-success:focus{box-shadow:0 0 0 .125rem rgba(44,157,69,.25);outline:0}.btn-success:focus-visible{outline:3px solid #3867ce;outline-offset:2px}.btn-success:focus:active{box-shadow:0 0 0 .25rem rgba(44,157,69,.25)}.btn-success:focus{background-color:#2c9d45;color:#fff}.btn-success:focus-visible{outline:3px solid #3867ce;outline-offset:2px}.btn-success.active:not(:disabled):not(.disabled){border:1px solid #25853b;background-color:#25853b;color:#fff}.show>.btn-success.dropdown-toggle{background-color:#25853b;color:#fff}.btn-success:hover:not(:disabled):not(.disabled){border:1px solid #25853b;background:#25853b radial-gradient(circle,transparent 1%,#25853b 1%) center/15000%;color:#fff;text-decoration:none}.btn-success:active:not(:disabled):not(.disabled){border:1px solid #25853b;background-color:#041007;background-size:100%;transition:background 0s;color:#fff}.scielo__theme--dark .btn-success{background-color:#2c9d45;border:1px solid #2c9d45;color:#333}.scielo__theme--dark .btn-success:focus{background-color:#2c9d45;color:#333}.scielo__theme--dark .btn-success.active:not(:disabled):not(.disabled){background-color:#25853b;color:#333}.scielo__theme--dark .btn-success:hover:not(:disabled):not(.disabled){border:1px solid #80c48f;background:#80c48f radial-gradient(circle,transparent 1%,#80c48f 1%) center/15000%;color:#333;text-decoration:none}.scielo__theme--dark .btn-success:active:not(:disabled):not(.disabled){border:1px solid #80c48f;background-color:#eaf5ec;background-size:100%;transition:background 0s;color:#333}.scielo__theme--light .btn-success{background-color:#2c9d45;border-color:1px solid #2c9d45;color:#fff}.scielo__theme--light .btn-success:focus{background-color:#2c9d45;color:#fff}.scielo__theme--light .btn-success.active:not(:disabled):not(.disabled){background-color:#4cac61;color:#fff}.scielo__theme--light .btn-success:hover:not(:disabled):not(.disabled){border:1px solid #25853b;background:#25853b radial-gradient(circle,transparent 1%,#25853b 1%) center/15000%;color:#fff;text-decoration:none}.scielo__theme--light .btn-success:active:not(:disabled):not(.disabled){border:1px solid #25853b;background-color:#041007;background-size:100%;transition:background 0s;color:#fff}.btn-danger{background-color:#c63800;border:1px solid #c63800;color:#fff}.btn-danger:focus{box-shadow:0 0 0 .125rem rgba(198,56,0,.25);outline:0}.btn-danger:focus-visible{outline:3px solid #3867ce;outline-offset:2px}.btn-danger:focus:active{box-shadow:0 0 0 .25rem rgba(198,56,0,.25)}.btn-danger:focus{background-color:#c63800;color:#fff}.btn-danger:focus-visible{outline:3px solid #3867ce;outline-offset:2px}.btn-danger.active:not(:disabled):not(.disabled){border:1px solid #a83000;background-color:#a83000;color:#fff}.show>.btn-danger.dropdown-toggle{background-color:#a83000;color:#fff}.btn-danger:hover:not(:disabled):not(.disabled){border:1px solid #a83000;background:#a83000 radial-gradient(circle,transparent 1%,#a83000 1%) center/15000%;color:#fff;text-decoration:none}.btn-danger:active:not(:disabled):not(.disabled){border:1px solid #a83000;background-color:#140600;background-size:100%;transition:background 0s;color:#fff}.scielo__theme--dark .btn-danger{background-color:#ff7e4a;border:1px solid #ff7e4a;color:#333}.scielo__theme--dark .btn-danger:focus{background-color:#ff7e4a;color:#333}.scielo__theme--dark .btn-danger.active:not(:disabled):not(.disabled){background-color:#d96b3f;color:#333}.scielo__theme--dark .btn-danger:hover:not(:disabled):not(.disabled){border:1px solid #ffb292;background:#ffb292 radial-gradient(circle,transparent 1%,#ffb292 1%) center/15000%;color:#333;text-decoration:none}.scielo__theme--dark .btn-danger:active:not(:disabled):not(.disabled){border:1px solid #ffb292;background-color:#fff2ed;background-size:100%;transition:background 0s;color:#333}.scielo__theme--light .btn-danger{background-color:#c63800;border-color:1px solid #c63800;color:#fff}.scielo__theme--light .btn-danger:focus{background-color:#c63800;color:#fff}.scielo__theme--light .btn-danger.active:not(:disabled):not(.disabled){background-color:#cf5626;color:#fff}.scielo__theme--light .btn-danger:hover:not(:disabled):not(.disabled){border:1px solid #a83000;background:#a83000 radial-gradient(circle,transparent 1%,#a83000 1%) center/15000%;color:#fff;text-decoration:none}.scielo__theme--light .btn-danger:active:not(:disabled):not(.disabled){border:1px solid #a83000;background-color:#140600;background-size:100%;transition:background 0s;color:#fff}.btn-warning{background-color:#b67f00;border:1px solid #b67f00;color:#fff}.btn-warning:focus{box-shadow:0 0 0 .125rem rgba(182,127,0,.25);outline:0}.btn-warning:focus-visible{outline:3px solid #3867ce;outline-offset:2px}.btn-warning:focus:active{box-shadow:0 0 0 .25rem rgba(182,127,0,.25)}.btn-warning:focus{background-color:#b67f00;color:#fff}.btn-warning:focus-visible{outline:3px solid #3867ce;outline-offset:2px}.btn-warning.active:not(:disabled):not(.disabled){border:1px solid #9b6c00;background-color:#9b6c00;color:#fff}.show>.btn-warning.dropdown-toggle{background-color:#9b6c00;color:#fff}.btn-warning:hover:not(:disabled):not(.disabled){border:1px solid #9b6c00;background:#9b6c00 radial-gradient(circle,transparent 1%,#9b6c00 1%) center/15000%;color:#fff;text-decoration:none}.btn-warning:active:not(:disabled):not(.disabled){border:1px solid #9b6c00;background-color:#120d00;background-size:100%;transition:background 0s;color:#fff}.scielo__theme--dark .btn-warning{background-color:#b67f00;border:1px solid #b67f00;color:#333}.scielo__theme--dark .btn-warning:focus{background-color:#b67f00;color:#333}.scielo__theme--dark .btn-warning.active:not(:disabled):not(.disabled){background-color:#9b6c00;color:#333}.scielo__theme--dark .btn-warning:hover:not(:disabled):not(.disabled){border:1px solid #d3b266;background:#d3b266 radial-gradient(circle,transparent 1%,#d3b266 1%) center/15000%;color:#333;text-decoration:none}.scielo__theme--dark .btn-warning:active:not(:disabled):not(.disabled){border:1px solid #d3b266;background-color:#f8f2e6;background-size:100%;transition:background 0s;color:#333}.scielo__theme--light .btn-warning{background-color:#b67f00;border-color:1px solid #b67f00;color:#fff}.scielo__theme--light .btn-warning:focus{background-color:#b67f00;color:#fff}.scielo__theme--light .btn-warning.active:not(:disabled):not(.disabled){background-color:#c19226;color:#fff}.scielo__theme--light .btn-warning:hover:not(:disabled):not(.disabled){border:1px solid #9b6c00;background:#9b6c00 radial-gradient(circle,transparent 1%,#9b6c00 1%) center/15000%;color:#fff;text-decoration:none}.scielo__theme--light .btn-warning:active:not(:disabled):not(.disabled){border:1px solid #9b6c00;background-color:#120d00;background-size:100%;transition:background 0s;color:#fff}.btn.disabled,.btn:disabled{pointer-events:auto;cursor:not-allowed;background-color:#f7f6f4;border:1px solid #ccc;color:rgba(0,0,0,.396);opacity:1}.btn.disabled:focus,.btn:disabled:focus{background-color:#f7f6f4;color:rgba(0,0,0,.396)}.btn.disabled:focus-visible,.btn:disabled:focus-visible{outline:3px solid #3867ce;outline-offset:2px}.btn.disabled.active:not(:disabled):not(.disabled),.btn:disabled.active:not(:disabled):not(.disabled){border:1px solid #d2d1cf;background-color:#d2d1cf;color:rgba(0,0,0,.396)}.show>.btn.disabled.dropdown-toggle,.show>.btn:disabled.dropdown-toggle{background-color:#d2d1cf;color:rgba(0,0,0,.396)}.btn.disabled:hover:not(:disabled):not(.disabled),.btn:disabled:hover:not(:disabled):not(.disabled){border:1px solid #d2d1cf;background:#d2d1cf radial-gradient(circle,transparent 1%,#d2d1cf 1%) center/15000%;color:rgba(0,0,0,.396);text-decoration:none}.btn.disabled:active:not(:disabled):not(.disabled),.btn:disabled:active:not(:disabled):not(.disabled){border:1px solid #d2d1cf;background-color:#191918;background-size:100%;transition:background 0s;color:rgba(0,0,0,.396)}.scielo__theme--dark .btn.disabled,.scielo__theme--dark .btn:disabled{background-color:rgba(255,255,255,.2);border:1px solid rgba(255,255,255,.3);color:#c4c4c4}.scielo__theme--dark .btn.disabled:focus,.scielo__theme--dark .btn:disabled:focus{background-color:rgba(255,255,255,.2);color:#c4c4c4}.scielo__theme--dark .btn.disabled.active:not(:disabled):not(.disabled),.scielo__theme--dark .btn:disabled.active:not(:disabled):not(.disabled){background-color:rgba(99,99,99,.32);color:#c4c4c4}.scielo__theme--dark .btn.disabled:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn:disabled:hover:not(:disabled):not(.disabled){border:1px solid rgba(255,255,255,.52);background:rgba(255,255,255,.52) radial-gradient(circle,transparent 1%,rgba(255,255,255,.52) 1%) center/15000%;color:#c4c4c4;text-decoration:none}.scielo__theme--dark .btn.disabled:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn:disabled:active:not(:disabled):not(.disabled){border:1px solid rgba(255,255,255,.52);background-color:rgba(255,255,255,.92);background-size:100%;transition:background 0s;color:#c4c4c4}.scielo__theme--light .btn.disabled,.scielo__theme--light .btn:disabled{background-color:#f7f6f4;border-color:1px solid #ccc;color:rgba(0,0,0,.396)}.scielo__theme--light .btn.disabled:focus,.scielo__theme--light .btn:disabled:focus{background-color:#f7f6f4;color:rgba(0,0,0,.396)}.scielo__theme--light .btn.disabled.active:not(:disabled):not(.disabled),.scielo__theme--light .btn:disabled.active:not(:disabled):not(.disabled){background-color:#f8f7f6;color:rgba(0,0,0,.396)}.scielo__theme--light .btn.disabled:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn:disabled:hover:not(:disabled):not(.disabled){border:1px solid #d2d1cf;background:#d2d1cf radial-gradient(circle,transparent 1%,#d2d1cf 1%) center/15000%;color:rgba(0,0,0,.396);text-decoration:none}.scielo__theme--light .btn.disabled:active:not(:disabled):not(.disabled),.scielo__theme--light .btn:disabled:active:not(:disabled):not(.disabled){border:1px solid #d2d1cf;background-color:#191918;background-size:100%;transition:background 0s;color:rgba(0,0,0,.396)}.btn-link{padding-left:1.5rem;padding-right:1.5rem;background-color:transparent;border:1px solid transparent;color:#3867ce;text-decoration:none}.btn-link:focus{box-shadow:0 0 0 .125rem rgba(56,103,206,.25);outline:0}.btn-link:focus:active{box-shadow:0 0 0 .25rem rgba(56,103,206,.25)}.btn-link:focus{background-color:transparent;border-color:transparent;color:#3867ce}.btn-link:hover:not(:disabled):not(.disabled){border:1px solid #fff;background:#fff radial-gradient(circle,transparent 1%,#fff 1%) center/15000%;color:#3867ce;text-decoration:none}.btn-link:active:not(:disabled):not(.disabled){border:1px solid #fff;background-color:rgba(56,103,206,.1);background-size:100%;transition:background 0s;color:#3867ce}.scielo__theme--dark .btn-link{background-color:transparent;border-color:transparent;color:#86acff}.scielo__theme--dark .btn-link:focus{color:#86acff;background-color:transparent;border-color:transparent}.scielo__theme--dark .btn-link:hover:not(:disabled):not(.disabled){border:1px solid #333;background:#333 radial-gradient(circle,transparent 1%,#333 1%) center/15000%;color:#fff;text-decoration:none}.scielo__theme--dark .btn-link:active:not(:disabled):not(.disabled){border:1px solid #333;background-color:rgba(239,238,236,.5);background-size:100%;transition:background 0s;color:#fff}.scielo__theme--light .btn-link{background-color:transparent;color:#3867ce}.scielo__theme--light .btn-link:focus{background-color:transparent;border-color:transparent;color:#3867ce}.scielo__theme--light .btn-link:hover:not(:disabled):not(.disabled){border:1px solid #fff;background:#fff radial-gradient(circle,transparent 1%,#fff 1%) center/15000%;color:#3867ce;text-decoration:none}.scielo__theme--light .btn-link:active:not(:disabled):not(.disabled){border:1px solid #fff;background-color:rgba(56,103,206,.1);background-size:100%;transition:background 0s;color:#3867ce}.btn-group-lg>.btn-link.btn,.btn-link.btn-lg{padding-left:1.875rem;padding-right:1.875rem}.btn-group-sm>.btn-link.btn,.btn-link.btn-sm{padding-left:1.125rem;padding-right:1.125rem}.btn-link.disabled,.btn-link:disabled{background-color:transparent;border:1px solid transparent;color:rgba(0,0,0,.396);opacity:1}.btn-link.disabled:focus,.btn-link:disabled:focus{background-color:transparent;border-color:transparent;color:rgba(0,0,0,.396)}.btn-link.disabled:hover:not(:disabled):not(.disabled),.btn-link:disabled:hover:not(:disabled):not(.disabled){border:1px solid #fff;background:#fff radial-gradient(circle,transparent 1%,#fff 1%) center/15000%;color:rgba(0,0,0,.396);text-decoration:none}.btn-link.disabled:active:not(:disabled):not(.disabled),.btn-link:disabled:active:not(:disabled):not(.disabled){border:1px solid #fff;background-color:rgba(56,103,206,.1);background-size:100%;transition:background 0s;color:rgba(0,0,0,.396)}.scielo__theme--dark .btn-link.disabled,.scielo__theme--dark .btn-link:disabled{background-color:transparent;border-color:transparent;color:rgba(255,255,255,.2)}.scielo__theme--dark .btn-link.disabled:focus,.scielo__theme--dark .btn-link:disabled:focus{color:rgba(255,255,255,.2);background-color:transparent;border-color:transparent}.scielo__theme--dark .btn-link.disabled:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn-link:disabled:hover:not(:disabled):not(.disabled){border:1px solid #333;background:#333 radial-gradient(circle,transparent 1%,#333 1%) center/15000%;color:#fff;text-decoration:none}.scielo__theme--dark .btn-link.disabled:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-link:disabled:active:not(:disabled):not(.disabled){border:1px solid #333;background-color:rgba(239,238,236,.5);background-size:100%;transition:background 0s;color:#fff}.scielo__theme--light .btn-link.disabled,.scielo__theme--light .btn-link:disabled{background-color:transparent;color:rgba(0,0,0,.396);opacity:1}.scielo__theme--light .btn-link.disabled:focus,.scielo__theme--light .btn-link:disabled:focus{background-color:transparent;border-color:transparent;color:rgba(0,0,0,.396)}.scielo__theme--light .btn-link.disabled:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn-link:disabled:hover:not(:disabled):not(.disabled){border:1px solid #fff;background:#fff radial-gradient(circle,transparent 1%,#fff 1%) center/15000%;color:rgba(0,0,0,.396);text-decoration:none}.scielo__theme--light .btn-link.disabled:active:not(:disabled):not(.disabled),.scielo__theme--light .btn-link:disabled:active:not(:disabled):not(.disabled){border:1px solid #fff;background-color:rgba(56,103,206,.1);background-size:100%;transition:background 0s;color:rgba(0,0,0,.396)}.btn-link:hover{background:0 0!important;border-color:transparent!important;text-decoration:underline!important}.btn-outline-light,.btn-outline-primary{background-color:transparent;border:1px solid #3867ce;color:#3867ce}.btn-outline-light:focus,.btn-outline-primary:focus{box-shadow:0 0 0 .125rem rgba(56,103,206,.25);outline:0}.btn-outline-light:focus:active,.btn-outline-primary:focus:active{box-shadow:0 0 0 .25rem rgba(56,103,206,.25)}.btn-outline-light:focus,.btn-outline-primary:focus{background-color:transparent;color:#3867ce}.btn-outline-light:focus,.btn-outline-light:hover,.btn-outline-primary:focus,.btn-outline-primary:hover{border-color:#3867ce}.btn-outline-light:hover:not(:disabled):not(.disabled),.btn-outline-primary:hover:not(:disabled):not(.disabled){border:1px solid #3058af;background:#3058af radial-gradient(circle,transparent 1%,#3058af 1%) center/15000%;color:#fff;text-decoration:none}.btn-outline-light:active:not(:disabled):not(.disabled),.btn-outline-primary:active:not(:disabled):not(.disabled){border:1px solid #3058af;background-color:#ebf0fa;background-size:100%;transition:background 0s;color:#fff}.scielo__theme--dark .btn-outline-light,.scielo__theme--dark .btn-outline-primary{background-color:transparent;border-color:#86acff;color:#86acff}.scielo__theme--dark .btn-outline-light:focus,.scielo__theme--dark .btn-outline-primary:focus{background-color:transparent;color:#86acff}.scielo__theme--dark .btn-outline-light:focus,.scielo__theme--dark .btn-outline-light:hover,.scielo__theme--dark .btn-outline-primary:focus,.scielo__theme--dark .btn-outline-primary:hover{border-color:#86acff}.scielo__theme--dark .btn-outline-light:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-primary:hover:not(:disabled):not(.disabled){border:1px solid #b6cdff;background:#b6cdff radial-gradient(circle,transparent 1%,#b6cdff 1%) center/15000%;color:#333;text-decoration:none}.scielo__theme--dark .btn-outline-light:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-primary:active:not(:disabled):not(.disabled){border:1px solid #b6cdff;background-color:#f3f7ff;background-size:100%;transition:background 0s;color:#333}.scielo__theme--light .btn-outline-light,.scielo__theme--light .btn-outline-primary{background-color:transparent;border:1px solid #3867ce;color:#3867ce}.scielo__theme--light .btn-outline-light:focus,.scielo__theme--light .btn-outline-primary:focus{background-color:transparent;color:#3867ce}.scielo__theme--light .btn-outline-light:focus,.scielo__theme--light .btn-outline-light:hover,.scielo__theme--light .btn-outline-primary:focus,.scielo__theme--light .btn-outline-primary:hover{border-color:#3867ce}.scielo__theme--light .btn-outline-light:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-primary:hover:not(:disabled):not(.disabled){border:1px solid #3058af;background:#3058af radial-gradient(circle,transparent 1%,#3058af 1%) center/15000%;color:#fff;text-decoration:none}.scielo__theme--light .btn-outline-light:active:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-primary:active:not(:disabled):not(.disabled){border:1px solid #3058af;background-color:#ebf0fa;background-size:100%;transition:background 0s;color:#fff}.btn-outline-dark,.btn-outline-secondary{background-color:transparent;border:1px solid 1px solid #ccc;color:#333}.btn-outline-dark:focus,.btn-outline-secondary:focus{box-shadow:0 0 0 .125rem rgba(204,204,204,.25);outline:0}.btn-outline-dark:focus:active,.btn-outline-secondary:focus:active{box-shadow:0 0 0 .25rem rgba(204,204,204,.25)}.btn-outline-dark:focus,.btn-outline-secondary:focus{background-color:transparent;color:#333}.btn-outline-dark:focus,.btn-outline-dark:hover,.btn-outline-secondary:focus,.btn-outline-secondary:hover{border-color:1px solid #ccc}.btn-outline-dark:hover:not(:disabled):not(.disabled),.btn-outline-secondary:hover:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background:#d9d9d9 radial-gradient(circle,transparent 1%,#d9d9d9 1%) center/15000%;color:#fff;text-decoration:none}.btn-outline-dark:active:not(:disabled):not(.disabled),.btn-outline-secondary:active:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background-color:#fff;background-size:100%;transition:background 0s;color:#fff}.btn-outline-dark:hover:not(:disabled):not(.disabled),.btn-outline-secondary:hover:not(:disabled):not(.disabled){border:1px solid #ccc;background:hoverBgColor radial-gradient(circle,transparent 1%,hoverBgColor 1%) center/15000%;color:#333}.btn-outline-dark:active:not(:disabled):not(.disabled),.btn-outline-secondary:active:not(:disabled):not(.disabled){border:1px solid #ccc;background-color:gray;background-size:100%;transition:background 0s;color:#333}.btn-outline-dark:focus,.btn-outline-secondary:focus{border-color:#ccc}.btn-outline-dark.dropdown-toggle.show,.btn-outline-secondary.dropdown-toggle.show{border-color:#ccc}.scielo__theme--dark .btn-outline-dark,.scielo__theme--dark .btn-outline-secondary{background-color:transparent;border-color:1px solid rgba(255,255,255,.3);color:#c4c4c4}.scielo__theme--dark .btn-outline-dark:focus,.scielo__theme--dark .btn-outline-secondary:focus{background-color:transparent;color:#c4c4c4}.scielo__theme--dark .btn-outline-dark:focus,.scielo__theme--dark .btn-outline-dark:hover,.scielo__theme--dark .btn-outline-secondary:focus,.scielo__theme--dark .btn-outline-secondary:hover{border-color:1px solid rgba(255,255,255,.3)}.scielo__theme--dark .btn-outline-dark:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-secondary:hover:not(:disabled):not(.disabled){border:1px solid #dcdcdc;background:#dcdcdc radial-gradient(circle,transparent 1%,#dcdcdc 1%) center/15000%;color:#333;text-decoration:none}.scielo__theme--dark .btn-outline-dark:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-secondary:active:not(:disabled):not(.disabled){border:1px solid #dcdcdc;background-color:#f9f9f9;background-size:100%;transition:background 0s;color:#333}.scielo__theme--dark .btn-outline-dark:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-secondary:hover:not(:disabled):not(.disabled){border:1px solid rgba(255,255,255,.3);background:hoverBgColor radial-gradient(circle,transparent 1%,hoverBgColor 1%) center/15000%;color:#333}.scielo__theme--dark .btn-outline-dark:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-secondary:active:not(:disabled):not(.disabled){border:1px solid rgba(255,255,255,.3);background-color:#f9f9f9;background-size:100%;transition:background 0s;color:#333}.scielo__theme--dark .btn-outline-dark:focus,.scielo__theme--dark .btn-outline-secondary:focus{border-color:rgba(255,255,255,.3)}.scielo__theme--dark .btn-outline-dark.dropdown-toggle.show,.scielo__theme--dark .btn-outline-secondary.dropdown-toggle.show{background:0 0;color:#c4c4c4}.scielo__theme--dark .btn-outline-dark.dropdown-toggle.show:focus,.scielo__theme--dark .btn-outline-secondary.dropdown-toggle.show:focus{box-shadow:0 0 0 .125rem rgba(204,204,204,.25)}.scielo__theme--light .btn-outline-dark,.scielo__theme--light .btn-outline-secondary{background-color:transparent;border:1px solid 1px solid #ccc;color:#333}.scielo__theme--light .btn-outline-dark:focus,.scielo__theme--light .btn-outline-secondary:focus{background-color:transparent;color:#333}.scielo__theme--light .btn-outline-dark:focus,.scielo__theme--light .btn-outline-dark:hover,.scielo__theme--light .btn-outline-secondary:focus,.scielo__theme--light .btn-outline-secondary:hover{border-color:1px solid #ccc}.scielo__theme--light .btn-outline-dark:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-secondary:hover:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background:#d9d9d9 radial-gradient(circle,transparent 1%,#d9d9d9 1%) center/15000%;color:#fff;text-decoration:none}.scielo__theme--light .btn-outline-dark:active:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-secondary:active:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background-color:#fff;background-size:100%;transition:background 0s;color:#fff}.scielo__theme--light .btn-outline-dark:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-secondary:hover:not(:disabled):not(.disabled){border:1px solid #ccc;background:hoverBgColor radial-gradient(circle,transparent 1%,hoverBgColor 1%) center/15000%;color:#333}.scielo__theme--light .btn-outline-dark:active:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-secondary:active:not(:disabled):not(.disabled){border:1px solid #ccc;background-color:gray;background-size:100%;transition:background 0s;color:#333}.scielo__theme--light .btn-outline-dark:focus,.scielo__theme--light .btn-outline-secondary:focus{border-color:#ccc}.btn-outline-info{background-color:transparent;border:1px solid #2195a9;color:#2299ad}.btn-outline-info:focus{box-shadow:0 0 0 .125rem rgba(33,149,169,.25);outline:0}.btn-outline-info:focus:active{box-shadow:0 0 0 .25rem rgba(33,149,169,.25)}.btn-outline-info:focus{background-color:transparent;color:#2299ad}.btn-outline-info:focus,.btn-outline-info:hover{border-color:#2195a9}.btn-outline-info:hover:not(:disabled):not(.disabled){border:1px solid #1c7f90;background:#1c7f90 radial-gradient(circle,transparent 1%,#1c7f90 1%) center/15000%;color:#fff;text-decoration:none}.btn-outline-info:active:not(:disabled):not(.disabled){border:1px solid #1c7f90;background-color:#e9f4f6;background-size:100%;transition:background 0s;color:#fff}.scielo__theme--dark .btn-outline-info{background-color:transparent;border-color:#2299ad;color:#2299ad}.scielo__theme--dark .btn-outline-info:focus{background-color:transparent;color:#2299ad}.scielo__theme--dark .btn-outline-info:focus,.scielo__theme--dark .btn-outline-info:hover{border-color:#2299ad}.scielo__theme--dark .btn-outline-info:hover:not(:disabled):not(.disabled){border:1px solid #7ac2ce;background:#7ac2ce radial-gradient(circle,transparent 1%,#7ac2ce 1%) center/15000%;color:#333;text-decoration:none}.scielo__theme--dark .btn-outline-info:active:not(:disabled):not(.disabled){border:1px solid #7ac2ce;background-color:#e9f5f7;background-size:100%;transition:background 0s;color:#333}.scielo__theme--light .btn-outline-info{background-color:transparent;border:1px solid #2195a9;color:#2299ad}.scielo__theme--light .btn-outline-info:focus{background-color:transparent;color:#2299ad}.scielo__theme--light .btn-outline-info:focus,.scielo__theme--light .btn-outline-info:hover{border-color:#2195a9}.scielo__theme--light .btn-outline-info:hover:not(:disabled):not(.disabled){border:1px solid #1c7f90;background:#1c7f90 radial-gradient(circle,transparent 1%,#1c7f90 1%) center/15000%;color:#fff;text-decoration:none}.scielo__theme--light .btn-outline-info:active:not(:disabled):not(.disabled){border:1px solid #1c7f90;background-color:#e9f4f6;background-size:100%;transition:background 0s;color:#fff}.btn-outline-success{background-color:transparent;border:1px solid #2c9d45;color:#2c9d45}.btn-outline-success:focus{box-shadow:0 0 0 .125rem rgba(44,157,69,.25);outline:0}.btn-outline-success:focus:active{box-shadow:0 0 0 .25rem rgba(44,157,69,.25)}.btn-outline-success:focus{background-color:transparent;color:#2c9d45}.btn-outline-success:focus,.btn-outline-success:hover{border-color:#2c9d45}.btn-outline-success:hover:not(:disabled):not(.disabled){border:1px solid #25853b;background:#25853b radial-gradient(circle,transparent 1%,#25853b 1%) center/15000%;color:#fff;text-decoration:none}.btn-outline-success:active:not(:disabled):not(.disabled){border:1px solid #25853b;background-color:#eaf5ec;background-size:100%;transition:background 0s;color:#fff}.scielo__theme--dark .btn-outline-success{background-color:transparent;border-color:#2c9d45;color:#2c9d45}.scielo__theme--dark .btn-outline-success:focus{background-color:transparent;color:#2c9d45}.scielo__theme--dark .btn-outline-success:focus,.scielo__theme--dark .btn-outline-success:hover{border-color:#2c9d45}.scielo__theme--dark .btn-outline-success:hover:not(:disabled):not(.disabled){border:1px solid #80c48f;background:#80c48f radial-gradient(circle,transparent 1%,#80c48f 1%) center/15000%;color:#333;text-decoration:none}.scielo__theme--dark .btn-outline-success:active:not(:disabled):not(.disabled){border:1px solid #80c48f;background-color:#eaf5ec;background-size:100%;transition:background 0s;color:#333}.scielo__theme--light .btn-outline-success{background-color:transparent;border:1px solid #2c9d45;color:#2c9d45}.scielo__theme--light .btn-outline-success:focus{background-color:transparent;color:#2c9d45}.scielo__theme--light .btn-outline-success:focus,.scielo__theme--light .btn-outline-success:hover{border-color:#2c9d45}.scielo__theme--light .btn-outline-success:hover:not(:disabled):not(.disabled){border:1px solid #25853b;background:#25853b radial-gradient(circle,transparent 1%,#25853b 1%) center/15000%;color:#fff;text-decoration:none}.scielo__theme--light .btn-outline-success:active:not(:disabled):not(.disabled){border:1px solid #25853b;background-color:#eaf5ec;background-size:100%;transition:background 0s;color:#fff}.btn-outline-danger{background-color:transparent;border:1px solid #c63800;color:#c63800}.btn-outline-danger:focus{box-shadow:0 0 0 .125rem rgba(198,56,0,.25);outline:0}.btn-outline-danger:focus:active{box-shadow:0 0 0 .25rem rgba(198,56,0,.25)}.btn-outline-danger:focus{background-color:transparent;color:#c63800}.btn-outline-danger:focus,.btn-outline-danger:hover{border-color:#c63800}.btn-outline-danger:hover:not(:disabled):not(.disabled){border:1px solid #a83000;background:#a83000 radial-gradient(circle,transparent 1%,#a83000 1%) center/15000%;color:#fff;text-decoration:none}.btn-outline-danger:active:not(:disabled):not(.disabled){border:1px solid #a83000;background-color:#f9ebe6;background-size:100%;transition:background 0s;color:#fff}.scielo__theme--dark .btn-outline-danger{background-color:transparent;border-color:#ff7e4a;color:#ff7e4a}.scielo__theme--dark .btn-outline-danger:focus{background-color:transparent;color:#ff7e4a}.scielo__theme--dark .btn-outline-danger:focus,.scielo__theme--dark .btn-outline-danger:hover{border-color:#ff7e4a}.scielo__theme--dark .btn-outline-danger:hover:not(:disabled):not(.disabled){border:1px solid #ffb292;background:#ffb292 radial-gradient(circle,transparent 1%,#ffb292 1%) center/15000%;color:#333;text-decoration:none}.scielo__theme--dark .btn-outline-danger:active:not(:disabled):not(.disabled){border:1px solid #ffb292;background-color:#fff2ed;background-size:100%;transition:background 0s;color:#333}.scielo__theme--light .btn-outline-danger{background-color:transparent;border:1px solid #c63800;color:#c63800}.scielo__theme--light .btn-outline-danger:focus{background-color:transparent;color:#c63800}.scielo__theme--light .btn-outline-danger:focus,.scielo__theme--light .btn-outline-danger:hover{border-color:#c63800}.scielo__theme--light .btn-outline-danger:hover:not(:disabled):not(.disabled){border:1px solid #a83000;background:#a83000 radial-gradient(circle,transparent 1%,#a83000 1%) center/15000%;color:#fff;text-decoration:none}.scielo__theme--light .btn-outline-danger:active:not(:disabled):not(.disabled){border:1px solid #a83000;background-color:#f9ebe6;background-size:100%;transition:background 0s;color:#fff}.btn-outline-warning{background-color:transparent;border:1px solid #b67f00;color:#b67f00}.btn-outline-warning:focus{box-shadow:0 0 0 .125rem rgba(182,127,0,.25);outline:0}.btn-outline-warning:focus:active{box-shadow:0 0 0 .25rem rgba(182,127,0,.25)}.btn-outline-warning:focus{background-color:transparent;color:#b67f00}.btn-outline-warning:focus,.btn-outline-warning:hover{border-color:#b67f00}.btn-outline-warning:hover:not(:disabled):not(.disabled){border:1px solid #9b6c00;background:#9b6c00 radial-gradient(circle,transparent 1%,#9b6c00 1%) center/15000%;color:#fff;text-decoration:none}.btn-outline-warning:active:not(:disabled):not(.disabled){border:1px solid #9b6c00;background-color:#f8f2e6;background-size:100%;transition:background 0s;color:#fff}.scielo__theme--dark .btn-outline-warning{background-color:transparent;border-color:#b67f00;color:#b67f00}.scielo__theme--dark .btn-outline-warning:focus{background-color:transparent;color:#b67f00}.scielo__theme--dark .btn-outline-warning:focus,.scielo__theme--dark .btn-outline-warning:hover{border-color:#b67f00}.scielo__theme--dark .btn-outline-warning:hover:not(:disabled):not(.disabled){border:1px solid #d3b266;background:#d3b266 radial-gradient(circle,transparent 1%,#d3b266 1%) center/15000%;color:#333;text-decoration:none}.scielo__theme--dark .btn-outline-warning:active:not(:disabled):not(.disabled){border:1px solid #d3b266;background-color:#f8f2e6;background-size:100%;transition:background 0s;color:#333}.scielo__theme--light .btn-outline-warning{background-color:transparent;border:1px solid #b67f00;color:#b67f00}.scielo__theme--light .btn-outline-warning:focus{background-color:transparent;color:#b67f00}.scielo__theme--light .btn-outline-warning:focus,.scielo__theme--light .btn-outline-warning:hover{border-color:#b67f00}.scielo__theme--light .btn-outline-warning:hover:not(:disabled):not(.disabled){border:1px solid #9b6c00;background:#9b6c00 radial-gradient(circle,transparent 1%,#9b6c00 1%) center/15000%;color:#fff;text-decoration:none}.scielo__theme--light .btn-outline-warning:active:not(:disabled):not(.disabled){border:1px solid #9b6c00;background-color:#f8f2e6;background-size:100%;transition:background 0s;color:#fff}.btn-outline-danger.disabled,.btn-outline-danger:disabled,.btn-outline-dark.disabled,.btn-outline-dark:disabled,.btn-outline-info.disabled,.btn-outline-info:disabled,.btn-outline-light.disabled,.btn-outline-light:disabled,.btn-outline-primary.disabled,.btn-outline-primary:disabled,.btn-outline-secondary.disabled,.btn-outline-secondary:disabled,.btn-outline-success.disabled,.btn-outline-success:disabled,.btn-outline-warning.disabled,.btn-outline-warning:disabled{background-color:transparent;border:1px solid #f7f6f4;color:rgba(0,0,0,.396);opacity:1}.btn-outline-danger.disabled:focus,.btn-outline-danger:disabled:focus,.btn-outline-dark.disabled:focus,.btn-outline-dark:disabled:focus,.btn-outline-info.disabled:focus,.btn-outline-info:disabled:focus,.btn-outline-light.disabled:focus,.btn-outline-light:disabled:focus,.btn-outline-primary.disabled:focus,.btn-outline-primary:disabled:focus,.btn-outline-secondary.disabled:focus,.btn-outline-secondary:disabled:focus,.btn-outline-success.disabled:focus,.btn-outline-success:disabled:focus,.btn-outline-warning.disabled:focus,.btn-outline-warning:disabled:focus{background-color:transparent;color:rgba(0,0,0,.396)}.btn-outline-danger.disabled:focus,.btn-outline-danger.disabled:hover,.btn-outline-danger:disabled:focus,.btn-outline-danger:disabled:hover,.btn-outline-dark.disabled:focus,.btn-outline-dark.disabled:hover,.btn-outline-dark:disabled:focus,.btn-outline-dark:disabled:hover,.btn-outline-info.disabled:focus,.btn-outline-info.disabled:hover,.btn-outline-info:disabled:focus,.btn-outline-info:disabled:hover,.btn-outline-light.disabled:focus,.btn-outline-light.disabled:hover,.btn-outline-light:disabled:focus,.btn-outline-light:disabled:hover,.btn-outline-primary.disabled:focus,.btn-outline-primary.disabled:hover,.btn-outline-primary:disabled:focus,.btn-outline-primary:disabled:hover,.btn-outline-secondary.disabled:focus,.btn-outline-secondary.disabled:hover,.btn-outline-secondary:disabled:focus,.btn-outline-secondary:disabled:hover,.btn-outline-success.disabled:focus,.btn-outline-success.disabled:hover,.btn-outline-success:disabled:focus,.btn-outline-success:disabled:hover,.btn-outline-warning.disabled:focus,.btn-outline-warning.disabled:hover,.btn-outline-warning:disabled:focus,.btn-outline-warning:disabled:hover{border-color:#f7f6f4}.btn-outline-danger.disabled:hover:not(:disabled):not(.disabled),.btn-outline-danger:disabled:hover:not(:disabled):not(.disabled),.btn-outline-dark.disabled:hover:not(:disabled):not(.disabled),.btn-outline-dark:disabled:hover:not(:disabled):not(.disabled),.btn-outline-info.disabled:hover:not(:disabled):not(.disabled),.btn-outline-info:disabled:hover:not(:disabled):not(.disabled),.btn-outline-light.disabled:hover:not(:disabled):not(.disabled),.btn-outline-light:disabled:hover:not(:disabled):not(.disabled),.btn-outline-primary.disabled:hover:not(:disabled):not(.disabled),.btn-outline-primary:disabled:hover:not(:disabled):not(.disabled),.btn-outline-secondary.disabled:hover:not(:disabled):not(.disabled),.btn-outline-secondary:disabled:hover:not(:disabled):not(.disabled),.btn-outline-success.disabled:hover:not(:disabled):not(.disabled),.btn-outline-success:disabled:hover:not(:disabled):not(.disabled),.btn-outline-warning.disabled:hover:not(:disabled):not(.disabled),.btn-outline-warning:disabled:hover:not(:disabled):not(.disabled){border:1px solid #3058af;background:#3058af radial-gradient(circle,transparent 1%,#3058af 1%) center/15000%;color:#fff;text-decoration:none}.btn-outline-danger.disabled:active:not(:disabled):not(.disabled),.btn-outline-danger:disabled:active:not(:disabled):not(.disabled),.btn-outline-dark.disabled:active:not(:disabled):not(.disabled),.btn-outline-dark:disabled:active:not(:disabled):not(.disabled),.btn-outline-info.disabled:active:not(:disabled):not(.disabled),.btn-outline-info:disabled:active:not(:disabled):not(.disabled),.btn-outline-light.disabled:active:not(:disabled):not(.disabled),.btn-outline-light:disabled:active:not(:disabled):not(.disabled),.btn-outline-primary.disabled:active:not(:disabled):not(.disabled),.btn-outline-primary:disabled:active:not(:disabled):not(.disabled),.btn-outline-secondary.disabled:active:not(:disabled):not(.disabled),.btn-outline-secondary:disabled:active:not(:disabled):not(.disabled),.btn-outline-success.disabled:active:not(:disabled):not(.disabled),.btn-outline-success:disabled:active:not(:disabled):not(.disabled),.btn-outline-warning.disabled:active:not(:disabled):not(.disabled),.btn-outline-warning:disabled:active:not(:disabled):not(.disabled){border:1px solid #3058af;background-color:#ebf0fa;background-size:100%;transition:background 0s;color:#fff}.scielo__theme--dark .btn-outline-danger.disabled,.scielo__theme--dark .btn-outline-danger:disabled,.scielo__theme--dark .btn-outline-dark.disabled,.scielo__theme--dark .btn-outline-dark:disabled,.scielo__theme--dark .btn-outline-info.disabled,.scielo__theme--dark .btn-outline-info:disabled,.scielo__theme--dark .btn-outline-light.disabled,.scielo__theme--dark .btn-outline-light:disabled,.scielo__theme--dark .btn-outline-primary.disabled,.scielo__theme--dark .btn-outline-primary:disabled,.scielo__theme--dark .btn-outline-secondary.disabled,.scielo__theme--dark .btn-outline-secondary:disabled,.scielo__theme--dark .btn-outline-success.disabled,.scielo__theme--dark .btn-outline-success:disabled,.scielo__theme--dark .btn-outline-warning.disabled,.scielo__theme--dark .btn-outline-warning:disabled{background-color:transparent;border-color:rgba(255,255,255,.2);color:#c4c4c4}.scielo__theme--dark .btn-outline-danger.disabled:focus,.scielo__theme--dark .btn-outline-danger:disabled:focus,.scielo__theme--dark .btn-outline-dark.disabled:focus,.scielo__theme--dark .btn-outline-dark:disabled:focus,.scielo__theme--dark .btn-outline-info.disabled:focus,.scielo__theme--dark .btn-outline-info:disabled:focus,.scielo__theme--dark .btn-outline-light.disabled:focus,.scielo__theme--dark .btn-outline-light:disabled:focus,.scielo__theme--dark .btn-outline-primary.disabled:focus,.scielo__theme--dark .btn-outline-primary:disabled:focus,.scielo__theme--dark .btn-outline-secondary.disabled:focus,.scielo__theme--dark .btn-outline-secondary:disabled:focus,.scielo__theme--dark .btn-outline-success.disabled:focus,.scielo__theme--dark .btn-outline-success:disabled:focus,.scielo__theme--dark .btn-outline-warning.disabled:focus,.scielo__theme--dark .btn-outline-warning:disabled:focus{background-color:transparent;color:#c4c4c4}.scielo__theme--dark .btn-outline-danger.disabled:focus,.scielo__theme--dark .btn-outline-danger.disabled:hover,.scielo__theme--dark .btn-outline-danger:disabled:focus,.scielo__theme--dark .btn-outline-danger:disabled:hover,.scielo__theme--dark .btn-outline-dark.disabled:focus,.scielo__theme--dark .btn-outline-dark.disabled:hover,.scielo__theme--dark .btn-outline-dark:disabled:focus,.scielo__theme--dark .btn-outline-dark:disabled:hover,.scielo__theme--dark .btn-outline-info.disabled:focus,.scielo__theme--dark .btn-outline-info.disabled:hover,.scielo__theme--dark .btn-outline-info:disabled:focus,.scielo__theme--dark .btn-outline-info:disabled:hover,.scielo__theme--dark .btn-outline-light.disabled:focus,.scielo__theme--dark .btn-outline-light.disabled:hover,.scielo__theme--dark .btn-outline-light:disabled:focus,.scielo__theme--dark .btn-outline-light:disabled:hover,.scielo__theme--dark .btn-outline-primary.disabled:focus,.scielo__theme--dark .btn-outline-primary.disabled:hover,.scielo__theme--dark .btn-outline-primary:disabled:focus,.scielo__theme--dark .btn-outline-primary:disabled:hover,.scielo__theme--dark .btn-outline-secondary.disabled:focus,.scielo__theme--dark .btn-outline-secondary.disabled:hover,.scielo__theme--dark .btn-outline-secondary:disabled:focus,.scielo__theme--dark .btn-outline-secondary:disabled:hover,.scielo__theme--dark .btn-outline-success.disabled:focus,.scielo__theme--dark .btn-outline-success.disabled:hover,.scielo__theme--dark .btn-outline-success:disabled:focus,.scielo__theme--dark .btn-outline-success:disabled:hover,.scielo__theme--dark .btn-outline-warning.disabled:focus,.scielo__theme--dark .btn-outline-warning.disabled:hover,.scielo__theme--dark .btn-outline-warning:disabled:focus,.scielo__theme--dark .btn-outline-warning:disabled:hover{border-color:rgba(255,255,255,.2)}.scielo__theme--dark .btn-outline-danger.disabled:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-danger:disabled:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-dark.disabled:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-dark:disabled:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-info.disabled:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-info:disabled:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-light.disabled:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-light:disabled:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-primary.disabled:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-primary:disabled:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-secondary.disabled:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-secondary:disabled:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-success.disabled:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-success:disabled:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-warning.disabled:hover:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-warning:disabled:hover:not(:disabled):not(.disabled){border:1px solid #b6cdff;background:#b6cdff radial-gradient(circle,transparent 1%,#b6cdff 1%) center/15000%;color:#333;text-decoration:none}.scielo__theme--dark .btn-outline-danger.disabled:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-danger:disabled:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-dark.disabled:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-dark:disabled:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-info.disabled:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-info:disabled:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-light.disabled:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-light:disabled:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-primary.disabled:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-primary:disabled:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-secondary.disabled:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-secondary:disabled:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-success.disabled:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-success:disabled:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-warning.disabled:active:not(:disabled):not(.disabled),.scielo__theme--dark .btn-outline-warning:disabled:active:not(:disabled):not(.disabled){border:1px solid #b6cdff;background-color:#f3f7ff;background-size:100%;transition:background 0s;color:#333}.scielo__theme--light .btn-outline-danger.disabled,.scielo__theme--light .btn-outline-danger:disabled,.scielo__theme--light .btn-outline-dark.disabled,.scielo__theme--light .btn-outline-dark:disabled,.scielo__theme--light .btn-outline-info.disabled,.scielo__theme--light .btn-outline-info:disabled,.scielo__theme--light .btn-outline-light.disabled,.scielo__theme--light .btn-outline-light:disabled,.scielo__theme--light .btn-outline-primary.disabled,.scielo__theme--light .btn-outline-primary:disabled,.scielo__theme--light .btn-outline-secondary.disabled,.scielo__theme--light .btn-outline-secondary:disabled,.scielo__theme--light .btn-outline-success.disabled,.scielo__theme--light .btn-outline-success:disabled,.scielo__theme--light .btn-outline-warning.disabled,.scielo__theme--light .btn-outline-warning:disabled{background-color:transparent;border:1px solid #f7f6f4;color:rgba(0,0,0,.396);opacity:1}.scielo__theme--light .btn-outline-danger.disabled:focus,.scielo__theme--light .btn-outline-danger:disabled:focus,.scielo__theme--light .btn-outline-dark.disabled:focus,.scielo__theme--light .btn-outline-dark:disabled:focus,.scielo__theme--light .btn-outline-info.disabled:focus,.scielo__theme--light .btn-outline-info:disabled:focus,.scielo__theme--light .btn-outline-light.disabled:focus,.scielo__theme--light .btn-outline-light:disabled:focus,.scielo__theme--light .btn-outline-primary.disabled:focus,.scielo__theme--light .btn-outline-primary:disabled:focus,.scielo__theme--light .btn-outline-secondary.disabled:focus,.scielo__theme--light .btn-outline-secondary:disabled:focus,.scielo__theme--light .btn-outline-success.disabled:focus,.scielo__theme--light .btn-outline-success:disabled:focus,.scielo__theme--light .btn-outline-warning.disabled:focus,.scielo__theme--light .btn-outline-warning:disabled:focus{background-color:transparent;color:rgba(0,0,0,.396)}.scielo__theme--light .btn-outline-danger.disabled:focus,.scielo__theme--light .btn-outline-danger.disabled:hover,.scielo__theme--light .btn-outline-danger:disabled:focus,.scielo__theme--light .btn-outline-danger:disabled:hover,.scielo__theme--light .btn-outline-dark.disabled:focus,.scielo__theme--light .btn-outline-dark.disabled:hover,.scielo__theme--light .btn-outline-dark:disabled:focus,.scielo__theme--light .btn-outline-dark:disabled:hover,.scielo__theme--light .btn-outline-info.disabled:focus,.scielo__theme--light .btn-outline-info.disabled:hover,.scielo__theme--light .btn-outline-info:disabled:focus,.scielo__theme--light .btn-outline-info:disabled:hover,.scielo__theme--light .btn-outline-light.disabled:focus,.scielo__theme--light .btn-outline-light.disabled:hover,.scielo__theme--light .btn-outline-light:disabled:focus,.scielo__theme--light .btn-outline-light:disabled:hover,.scielo__theme--light .btn-outline-primary.disabled:focus,.scielo__theme--light .btn-outline-primary.disabled:hover,.scielo__theme--light .btn-outline-primary:disabled:focus,.scielo__theme--light .btn-outline-primary:disabled:hover,.scielo__theme--light .btn-outline-secondary.disabled:focus,.scielo__theme--light .btn-outline-secondary.disabled:hover,.scielo__theme--light .btn-outline-secondary:disabled:focus,.scielo__theme--light .btn-outline-secondary:disabled:hover,.scielo__theme--light .btn-outline-success.disabled:focus,.scielo__theme--light .btn-outline-success.disabled:hover,.scielo__theme--light .btn-outline-success:disabled:focus,.scielo__theme--light .btn-outline-success:disabled:hover,.scielo__theme--light .btn-outline-warning.disabled:focus,.scielo__theme--light .btn-outline-warning.disabled:hover,.scielo__theme--light .btn-outline-warning:disabled:focus,.scielo__theme--light .btn-outline-warning:disabled:hover{border-color:#f7f6f4}.scielo__theme--light .btn-outline-danger.disabled:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-danger:disabled:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-dark.disabled:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-dark:disabled:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-info.disabled:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-info:disabled:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-light.disabled:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-light:disabled:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-primary.disabled:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-primary:disabled:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-secondary.disabled:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-secondary:disabled:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-success.disabled:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-success:disabled:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-warning.disabled:hover:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-warning:disabled:hover:not(:disabled):not(.disabled){border:1px solid #3058af;background:#3058af radial-gradient(circle,transparent 1%,#3058af 1%) center/15000%;color:#fff;text-decoration:none}.scielo__theme--light .btn-outline-danger.disabled:active:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-danger:disabled:active:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-dark.disabled:active:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-dark:disabled:active:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-info.disabled:active:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-info:disabled:active:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-light.disabled:active:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-light:disabled:active:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-primary.disabled:active:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-primary:disabled:active:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-secondary.disabled:active:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-secondary:disabled:active:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-success.disabled:active:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-success:disabled:active:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-warning.disabled:active:not(:disabled):not(.disabled),.scielo__theme--light .btn-outline-warning:disabled:active:not(:disabled):not(.disabled){border:1px solid #3058af;background-color:#ebf0fa;background-size:100%;transition:background 0s;color:#fff}.btn[class*=scielo__btn-with-icon--left]{padding-left:2rem}.btn[class*=scielo__btn-with-icon--left] [class^=material-icons]{position:absolute;top:50%;transform:translateY(-50%);width:1.25rem;height:1.25rem;font-size:1.25rem;line-height:1.25rem}.btn[class*=scielo__btn-with-icon--left] [class^=material-icons]:before{vertical-align:top}.btn[class*=scielo__btn-with-icon--left] [class^=material-icons]{left:.5rem}.btn[class*=scielo__btn-with-icon--right]{padding-right:2rem}.btn[class*=scielo__btn-with-icon--right] [class^=material-icons]{position:absolute;top:50%;transform:translateY(-50%);width:1.25rem;height:1.25rem;font-size:1.25rem;line-height:1.25rem}.btn[class*=scielo__btn-with-icon--right] [class^=material-icons]:before{vertical-align:top}.btn[class*=scielo__btn-with-icon--right] [class^=material-icons]{right:.5rem}.btn[class*=scielo__btn-with-icon--only]{padding:0;width:2.5rem}.btn[class*=scielo__btn-with-icon--only] [class^=material-icons]{position:absolute;top:50%;transform:translateY(-50%);width:1.25rem;height:1.25rem;font-size:1.25rem;line-height:1.25rem}.btn[class*=scielo__btn-with-icon--only] [class^=material-icons]:before{vertical-align:top}.btn[class*=scielo__btn-with-icon--only] [class^=material-icons]{top:50%;left:50%;transform:translate(-50%,-50%)}.btn.dropdown-toggle[class*=scielo__btn-with-icon--only]{width:3rem;display:flex;justify-content:center;align-items:center;padding-left:2rem;padding-right:1.5rem}.btn.dropdown-toggle[class*=scielo__btn-with-icon--only]:after{right:0;position:static;transform:none;margin:0}.btn.dropdown-toggle[class*=scielo__btn-with-icon--only] [class^=material-icons]{position:static;transform:none}.btn.dropdown-toggle:after{position:absolute;top:50%;transform:translateY(-50%);font-family:'Material Icons Outlined';content:"arrow_drop_down";color:inherit;border:0;line-height:1.5rem!important;text-align:center;right:1rem;width:1.25rem;height:1.25rem;font-size:1.25rem;line-height:1.25rem;transition:.3s all ease-out}.btn.dropdown-toggle[class*=scielo__btn-with-icon--left]{padding-right:1.5rem}.btn.dropdown-toggle[class*=scielo__btn-with-icon--left].btn-link{padding-left:2.625rem;padding-right:2.625rem}.btn.dropdown-toggle[class*=scielo__btn-with-icon--left]:after{right:.3rem}.btn-group-lg>.btn,.btn-group.btn-group-lg>.btn,.btn-lg{padding:.75rem 1.2rem;border-radius:.25rem;line-height:1.5rem;height:3rem;font-size:1.25rem}.btn-group-lg>.btn[class*=scielo__btn-with-icon--left],.btn-lg[class*=scielo__btn-with-icon--left]{padding-left:2.5rem}.btn-group-lg>.btn[class*=scielo__btn-with-icon--left] [class^=material-icons],.btn-lg[class*=scielo__btn-with-icon--left] [class^=material-icons]{position:absolute;top:50%;transform:translateY(-50%);width:1.5rem;height:1.5rem;font-size:1.5rem;line-height:1.5rem}.btn-group-lg>.btn[class*=scielo__btn-with-icon--left] [class^=material-icons]:before,.btn-lg[class*=scielo__btn-with-icon--left] [class^=material-icons]:before{vertical-align:top}.btn-group-lg>.btn[class*=scielo__btn-with-icon--left] [class^=material-icons],.btn-lg[class*=scielo__btn-with-icon--left] [class^=material-icons]{left:.625rem}.btn-group-lg>.btn[class*=scielo__btn-with-icon--right],.btn-lg[class*=scielo__btn-with-icon--right]{padding-right:2.5rem}.btn-group-lg>.btn[class*=scielo__btn-with-icon--right] [class^=material-icons],.btn-lg[class*=scielo__btn-with-icon--right] [class^=material-icons]{position:absolute;top:50%;transform:translateY(-50%);width:1.5rem;height:1.5rem;font-size:1.5rem;line-height:1.5rem}.btn-group-lg>.btn[class*=scielo__btn-with-icon--right] [class^=material-icons]:before,.btn-lg[class*=scielo__btn-with-icon--right] [class^=material-icons]:before{vertical-align:top}.btn-group-lg>.btn[class*=scielo__btn-with-icon--right] [class^=material-icons],.btn-lg[class*=scielo__btn-with-icon--right] [class^=material-icons]{right:.625rem}.btn-group-lg>.btn[class*=scielo__btn-with-icon--only],.btn-lg[class*=scielo__btn-with-icon--only]{padding:0;width:3rem}.btn-group-lg>.btn[class*=scielo__btn-with-icon--only] [class^=material-icons],.btn-lg[class*=scielo__btn-with-icon--only] [class^=material-icons]{position:absolute;top:50%;transform:translateY(-50%);width:1.5rem;height:1.5rem;font-size:1.5rem;line-height:1.5rem}.btn-group-lg>.btn[class*=scielo__btn-with-icon--only] [class^=material-icons]:before,.btn-lg[class*=scielo__btn-with-icon--only] [class^=material-icons]:before{vertical-align:top}.btn-group-lg>.btn[class*=scielo__btn-with-icon--only] [class^=material-icons],.btn-lg[class*=scielo__btn-with-icon--only] [class^=material-icons]{top:50%;left:50%;transform:translate(-50%,-50%)}.btn-group-lg>.dropdown-toggle.btn[class*=scielo__btn-with-icon--only],.btn-lg.dropdown-toggle[class*=scielo__btn-with-icon--only]{width:3rem;padding-left:2.5rem;padding-right:2.5rem;display:flex;justify-content:center;align-items:center}.btn-group-lg>.dropdown-toggle.btn:after,.btn-lg.dropdown-toggle:after{right:1.25rem;width:1.5rem;height:1.5rem;font-size:1.5rem;line-height:1.5rem}.btn-group-sm>.btn,.btn-group.btn-group-sm>.btn,.btn-sm{padding:.5rem .8rem;border-radius:.25rem;line-height:1rem;height:2rem}.dropdown>.btn{padding-right:2.5rem}.dropdown.show .btn{border-top-left-radius:1.5rem;border-top-right-radius:1.5rem;border-bottom-left-radius:.25rem;border-bottom-right-radius:.25rem}.dropdown.show .btn:after{transform:rotate(180deg) translateY(50%)}.dropdown .dropdown-menu{background:#fff;border-color:#ccc}.scielo__theme--dark .dropdown .dropdown-menu{background:#333;border-color:rgba(255,255,255,.3)}.scielo__theme--light .dropdown .dropdown-menu{background:#fff;border-color:#ccc}.dropdown .dropdown-menu>a{color:#333}.dropdown .dropdown-menu>a:hover{background:#f7f6f4}.scielo__theme--dark .dropdown .dropdown-menu>a{color:#c4c4c4}.scielo__theme--dark .dropdown .dropdown-menu>a:hover{background:#414141}.scielo__theme--light .dropdown .dropdown-menu>a{color:#333}.scielo__theme--light .dropdown .dropdown-menu>a:hover{background:#f7f6f4}.copyLink{position:relative;cursor:pointer}.copyLink:after{font-family:'Material Icons Outlined';content:"check";position:absolute;background:#2c9d45;top:100%;left:0;bottom:0;text-align:center;width:100%;color:#fff;font-size:20px;display:block;padding-top:.5rem;text-align:center;transition:all .3s ease-out,text-indent .3s ease-out}.scielo__theme--dark .copyLink:after{background:#2c9d45;color:#333}.scielo__theme--light .copyLink:after{background:#2c9d45;color:#fff}.copyLink.copyFeedback:after{top:0;visibility:visible}.floatingBtnError{position:fixed;right:calc(0px + var(--bs-gutter-x,1.5rem));top:35%;transform:rotate(-90deg);display:none;transform-origin:right center;margin:0}@media (min-width:1024px){.floatingBtnError{display:block}}.fixed-top .dropdown-item{color:#3867ce}.btn-group>.btn{font-weight:400!important;font-size:1rem;letter-spacing:.1px!important;white-space:nowrap}.btn-group>.btn:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0;margin-left:0}.btn-group>.btn:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0;margin-right:-1px}.btn-group>.btn:not(:first-child):not(.scielo__btn-with-icon--left):not(.scielo__btn-with-icon--only):not(.arrow-only){padding-left:1.5rem}.btn-group>.btn:not(:last-child):not(.scielo__btn-with-icon--right):not(.scielo__btn-with-icon--only){padding-right:1.5rem}.btn-group>.btn-group>.btn{border-radius:0}.btn-group>.btn-group>.btn.dropdown-toggle{padding-right:3rem!important}.btn-group>.btn-group:first-child>.btn.dropdown-toggle{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.btn-group>.btn-group:last-child>.btn.dropdown-toggle{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.btn-group>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0;margin-left:0;padding-left:1.5rem}.btn-group>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0;margin-right:0;padding-right:1.5rem}.btn-group.btn-group-sm>.btn:not(:first-child):not(.scielo__btn-with-icon--left):not(.scielo__btn-with-icon--only):not(.arrow-only){padding-left:1.125rem}.btn-group.btn-group-sm>.btn:not(:last-child):not(.scielo__btn-with-icon--right):not(.scielo__btn-with-icon--only){padding-right:1.125rem}.btn-group.btn-group-lg>.btn:not(:first-child):not(.scielo__btn-with-icon--left):not(.scielo__btn-with-icon--only):not(.arrow-only){padding-left:1.875rem}.btn-group.btn-group-lg>.btn:not(:last-child):not(.scielo__btn-with-icon--right):not(.scielo__btn-with-icon--only){padding-right:1.875rem}.btn-group-vertical>.btn{margin-bottom:0}.btn-group-vertical>.btn:first-child{border-top-left-radius:.1875rem;border-top-right-radius:.1875rem}.btn-group-vertical>.btn:last-child{border-bottom-left-radius:.1875rem;border-bottom-right-radius:.1875rem}.btn-group-vertical>.btn-group>.btn{padding-left:3rem!important;padding-right:3rem!important}.btn-group-vertical>.btn-group:first-child>.btn{border-top-left-radius:.1875rem;border-top-right-radius:.1875rem}.btn-group-vertical>.btn-group:last-child>.btn{border-bottom-left-radius:.1875rem;border-bottom-right-radius:.1875rem}.scielo__floatingMenuCtt{position:fixed;bottom:30px;width:auto;height:auto;margin:0}.scielo__floatingMenuCtt .material-icons-outlined{vertical-align:baseline}.scielo__floatingMenuCtt>a{padding-top:6px}.scielo__floatingMenu{transition:all .5s;box-sizing:border-box;z-index:1001;padding-left:0;white-space:nowrap;list-style:none;opacity:1;bottom:auto;opacity:1;margin:0;display:inline-block}.scielo__floatingMenu .fm-wrap{padding:0;margin:0}@media (min-width:576px){.scielo__floatingMenu .fm-wrap{padding:25px 25px 25px 0;margin:-25px -25px -25px 0}}.scielo__floatingMenu .fm-button-child,.scielo__floatingMenu .fm-button-main{display:inline-block;position:relative;padding:0;color:#fff;cursor:pointer;outline:0;background-color:#3867ce;border:none;border-radius:50%;box-shadow:0 0 4px rgba(0,0,0,.14),0 4px 8px rgba(0,0,0,.28);-webkit-user-drag:none}.scielo__theme--dark .scielo__floatingMenu .fm-button-child,.scielo__theme--dark .scielo__floatingMenu .fm-button-main{background:#86acff;color:#333}.scielo__theme--light .scielo__floatingMenu .fm-button-child,.scielo__theme--light .scielo__floatingMenu .fm-button-main{background-color:#3867ce;color:#fff}.scielo__floatingMenu .fm-button-main{width:56px;height:56px;z-index:20;padding-top:17px;padding-left:16px}.scielo__floatingMenu .fm-button-main .glyphFloatMenu{position:absolute;width:53px;height:56px;font-size:32px;line-height:56px;text-align:center}.scielo__floatingMenu .fm-button-main .material-icons-outlined-menu-close,.scielo__floatingMenu .fm-button-main .sci-ico-floatingMenuClose{opacity:0}.scielo__floatingMenu .fm-button-child{width:40px;height:40px;line-height:40px;text-align:center;-webkit-animation-fill-mode:both;-moz-animation-fill-mode:both;-ms-animation-fill-mode:both;-o-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.3s;-moz-animation-duration:.3s;-ms-animation-duration:.3s;-o-animation-duration:.3s;animation-duration:.3s;-o-transition:all .3s ease-out,text-indent .3s ease-out;-ms-transition:all .3s ease-out,text-indent .3s ease-out;-moz-transition:all .3s ease-out,text-indent .3s ease-out;-webkit-transition:all .3s ease-out,text-indent .3s ease-out;transition:all .3s ease-out,text-indent .3s ease-out;margin-top:1px;padding-top:6px}.scielo__floatingMenu .fm-button-child .glyphFloatMenu{font-size:24px}.scielo__floatingMenu .fm-button-child:hover{background-color:#3058af}.scielo__theme--dark .scielo__floatingMenu .fm-button-child:hover{background-color:#b6cdff;color:#333}.scielo__theme--light .scielo__floatingMenu .fm-button-child:hover{background-color:#3058af;color:#fff}.scielo__floatingMenu .fm-list{position:absolute;bottom:42px;width:56px;min-height:56px;margin:0}@media (min-width:576px){.scielo__floatingMenu .fm-list{margin-left:8px}}.scielo__floatingMenu .fm-list li{box-sizing:border-box;position:absolute;top:30px;left:8px;display:block;padding:9px 0 2px 0;margin:0;width:50px;height:auto}@media (min-width:576px){.scielo__floatingMenu .fm-list li{top:41px;left:6px}}@media (max-width:575.98px){.scielo__floatingMenu .fm-list li a:after{content:attr(data-mobile-tooltip);color:#fff;background:#333;position:absolute;margin-left:16px;display:inline-block;width:auto;height:auto;text-align:left;padding:5px;line-height:100%;border-radius:4px;font-size:.75rem;margin-top:3px}.scielo__theme--dark .scielo__floatingMenu .fm-list li a:after{background:#fff;color:#333}.scielo__theme--light .scielo__floatingMenu .fm-list li a:after{background:#333;color:#fff}.scielo__floatingMenu .fm-list li a:before{content:'';border-right:4px solid #333;border-left:4px solid transparent;border-top:4px solid transparent;border-bottom:4px solid transparent;margin-left:13px;margin-top:10px;position:absolute;margin-left:32px}.scielo__theme--dark .scielo__floatingMenu .fm-list li a:before{border-right:4px solid #fff}.scielo__theme--light .scielo__floatingMenu .fm-list li a:before{border-right:4px solid #333}}.scielo__floatingMenu:hover .fm-button-main{background-color:#fff;padding-top:17px;padding-left:16px}.scielo__floatingMenu:hover .material-icons-outlined-menu-default,.scielo__floatingMenu:hover .sci-ico-floatingMenuDefault{opacity:0;display:none}.scielo__floatingMenu:hover .material-icons-outlined-menu-close,.scielo__floatingMenu:hover .sci-ico-floatingMenuClose{opacity:1;color:#3867ce}.scielo__floatingMenu.fm-slidein .fm-list li{display:block;opacity:0;transition:all .5s}.scielo__floatingMenu.fm-slidein[data-fm-toogle=hover]:hover .fm-list li{opacity:1}.scielo__floatingMenu.fm-slidein[data-fm-toogle=hover]:hover .fm-list li:nth-child(1){-webkit-transform:translateX(50px);transform:translateX(50px)}@media (max-width:575.98px){.scielo__floatingMenu.fm-slidein[data-fm-toogle=hover]:hover .fm-list li:nth-child(1){-webkit-transform:translateY(-50px);transform:translateY(-50px)}}.scielo__floatingMenu.fm-slidein[data-fm-toogle=hover]:hover .fm-list li:nth-child(2){-webkit-transform:translateX(100px);transform:translateX(100px)}@media (max-width:575.98px){.scielo__floatingMenu.fm-slidein[data-fm-toogle=hover]:hover .fm-list li:nth-child(2){-webkit-transform:translateY(-100px);transform:translateY(-100px)}}.scielo__floatingMenu.fm-slidein[data-fm-toogle=hover]:hover .fm-list li:nth-child(3){-webkit-transform:translateX(150px);transform:translateX(150px)}@media (max-width:575.98px){.scielo__floatingMenu.fm-slidein[data-fm-toogle=hover]:hover .fm-list li:nth-child(3){-webkit-transform:translateY(-150px);transform:translateY(-150px)}}.scielo__floatingMenu.fm-slidein[data-fm-toogle=hover]:hover .fm-list li:nth-child(4){-webkit-transform:translateX(200px);transform:translateX(200px)}@media (max-width:575.98px){.scielo__floatingMenu.fm-slidein[data-fm-toogle=hover]:hover .fm-list li:nth-child(4){-webkit-transform:translateY(-200px);transform:translateY(-200px)}}.scielo__floatingMenu.fm-slidein[data-fm-toogle=hover]:hover .fm-list li:nth-child(5){-webkit-transform:translateX(250px);transform:translateX(250px)}@media (max-width:575.98px){.scielo__floatingMenu.fm-slidein[data-fm-toogle=hover]:hover .fm-list li:nth-child(5){-webkit-transform:translateY(-250px);transform:translateY(-250px)}}.scielo__floatingMenu.fm-slidein[data-fm-toogle=hover]:hover .fm-list li:nth-child(6){-webkit-transform:translateX(300px);transform:translateX(300px)}@media (max-width:575.98px){.scielo__floatingMenu.fm-slidein[data-fm-toogle=hover]:hover .fm-list li:nth-child(6){-webkit-transform:translateY(-300px);transform:translateY(-300px)}}.scielo__floatingMenu.fm-slidein[data-fm-toogle=hover]:hover .fm-list li:nth-child(7){-webkit-transform:translateX(350px);transform:translateX(350px)}@media (max-width:575.98px){.scielo__floatingMenu.fm-slidein[data-fm-toogle=hover]:hover .fm-list li:nth-child(7){-webkit-transform:translateY(-350px);transform:translateY(-350px)}}.scielo__floatingMenuItem{opacity:1;color:#fff;background-color:#3867ce;border-radius:50%;display:inline-block;width:40px;height:40px;line-height:40px;text-align:center;-webkit-animation-fill-mode:both;-moz-animation-fill-mode:both;-ms-animation-fill-mode:both;-o-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.3s;-moz-animation-duration:.3s;-ms-animation-duration:.3s;-o-animation-duration:.3s;animation-duration:.3s;-o-transition:all .3s ease-out,text-indent .3s ease-out;-ms-transition:all .3s ease-out,text-indent .3s ease-out;-moz-transition:all .3s ease-out,text-indent .3s ease-out;-webkit-transition:all .3s ease-out,text-indent .3s ease-out;transition:all .3s ease-out,text-indent .3s ease-out;box-shadow:0 0 4px rgba(0,0,0,.14),0 4px 8px rgba(0,0,0,.28);margin-right:4px}.scielo__theme--dark .scielo__floatingMenuItem{background:#86acff;color:#333}.scielo__theme--light .scielo__floatingMenuItem{background:#3867ce;color:#fff}.scielo__floatingMenuItem .glyphFloatMenu{font-size:24px}.scielo__floatingMenuItem:hover{background-color:#3058af;color:#fff}.scielo__theme--dark .scielo__floatingMenuItem:hover{background-color:#b6cdff;color:#333}.scielo__theme--light .scielo__floatingMenuItem:hover{background-color:#3058af;color:#fff}.scielo__floatingMenuCttJs{position:fixed;bottom:30px;width:auto;height:auto;margin:0}.scielo__floatingMenuCttJs .material-icons-outlined{vertical-align:baseline}.scielo__floatingMenuCttJs>a{padding-top:6px}.scielo__floatingMenuJs{transition:all .5s;box-sizing:border-box;z-index:1001;padding-left:0;white-space:nowrap;list-style:none;opacity:1;bottom:auto;opacity:1;margin:0;display:inline-block}.scielo__floatingMenuJs .fm-wrap{padding:0;margin:0}@media (min-width:576px){.scielo__floatingMenuJs .fm-wrap{padding:25px 25px 25px 0;margin:-25px -25px -25px 0}}.scielo__floatingMenuJs .fm-button-child,.scielo__floatingMenuJs .fm-button-close,.scielo__floatingMenuJs .fm-button-main{display:inline-block;position:relative;padding:0;color:#fff;cursor:pointer;outline:0;background-color:#3867ce;border:none;border-radius:50%;box-shadow:0 0 4px rgba(0,0,0,.14),0 4px 8px rgba(0,0,0,.28);-webkit-user-drag:none}.scielo__theme--dark .scielo__floatingMenuJs .fm-button-child,.scielo__theme--dark .scielo__floatingMenuJs .fm-button-close,.scielo__theme--dark .scielo__floatingMenuJs .fm-button-main{background:#86acff;color:#333}.scielo__theme--light .scielo__floatingMenuJs .fm-button-child,.scielo__theme--light .scielo__floatingMenuJs .fm-button-close,.scielo__theme--light .scielo__floatingMenuJs .fm-button-main{background-color:#3867ce;color:#fff}.scielo__floatingMenuJs .fm-button-close,.scielo__floatingMenuJs .fm-button-main{width:56px;height:56px;z-index:20;padding-top:17px;padding-left:16px}.scielo__floatingMenuJs .fm-button-close .glyphFloatMenu,.scielo__floatingMenuJs .fm-button-main .glyphFloatMenu{position:absolute;width:53px;height:56px;font-size:32px;line-height:56px;text-align:center}.scielo__floatingMenuJs .fm-button-close{background:#fff;color:#3867ce}.scielo__floatingMenuJs .fm-button-child{width:40px;height:40px;line-height:40px;text-align:center;-webkit-animation-fill-mode:both;-moz-animation-fill-mode:both;-ms-animation-fill-mode:both;-o-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.3s;-moz-animation-duration:.3s;-ms-animation-duration:.3s;-o-animation-duration:.3s;animation-duration:.3s;-o-transition:all .3s ease-out,text-indent .3s ease-out;-ms-transition:all .3s ease-out,text-indent .3s ease-out;-moz-transition:all .3s ease-out,text-indent .3s ease-out;-webkit-transition:all .3s ease-out,text-indent .3s ease-out;transition:all .3s ease-out,text-indent .3s ease-out;margin-top:1px;padding-top:6px}.scielo__floatingMenuJs .fm-button-child .glyphFloatMenu{font-size:24px}.scielo__floatingMenuJs .fm-button-child:hover{background-color:#3058af}.scielo__theme--dark .scielo__floatingMenuJs .fm-button-child:hover{background-color:#b6cdff;color:#333}.scielo__theme--light .scielo__floatingMenuJs .fm-button-child:hover{background-color:#3058af;color:#fff}.scielo__floatingMenuJs .fm-list{position:absolute;bottom:42px;width:56px;min-height:56px;margin:0}@media (min-width:576px){.scielo__floatingMenuJs .fm-list{margin-left:8px}}.scielo__floatingMenuJs .fm-list li{box-sizing:border-box;position:absolute;top:30px;left:8px;padding:9px 0 2px 0;margin:0;width:50px;height:auto;list-style:none}@media (min-width:576px){.scielo__floatingMenuJs .fm-list li{top:41px;left:6px}}@media (max-width:575.98px){.scielo__floatingMenuJs .fm-list li a{display:none}.scielo__floatingMenuJs .fm-list li a:after{content:attr(data-mobile-tooltip);color:#fff;background:#333;position:absolute;margin-left:16px;display:inline-block;width:auto;height:auto;text-align:left;padding:5px;line-height:100%;border-radius:4px;font-size:.75rem;margin-top:3px}.scielo__theme--dark .scielo__floatingMenuJs .fm-list li a:after{background:#fff;color:#333}.scielo__theme--light .scielo__floatingMenuJs .fm-list li a:after{background:#333;color:#fff}.scielo__floatingMenuJs .fm-list li a:before{content:'';border-right:4px solid #333;border-left:4px solid transparent;border-top:4px solid transparent;border-bottom:4px solid transparent;margin-left:13px;margin-top:10px;position:absolute;margin-left:32px}.scielo__theme--dark .scielo__floatingMenuJs .fm-list li a:before{border-right:4px solid #fff}.scielo__theme--light .scielo__floatingMenuJs .fm-list li a:before{border-right:4px solid #333}}.scielo__floatingMenuCttJs2{position:fixed;bottom:30px;width:auto;height:auto;margin:0}.scielo__floatingMenuCttJs2 .material-icons-outlined{vertical-align:baseline}.scielo__floatingMenuCttJs2>a{padding-top:6px}.scielo__floatingMenuJs2{transition:all .5s;box-sizing:border-box;z-index:1001;padding-left:0;white-space:nowrap;list-style:none;opacity:1;bottom:auto;opacity:1;margin:0;display:inline-block}.scielo__floatingMenuJs2 .fm-wrap{padding:0;margin:0}@media (min-width:576px){.scielo__floatingMenuJs2 .fm-wrap{padding:25px 25px 25px 0;margin:-25px -25px -25px 0}}.scielo__floatingMenuJs2 .fm-button-child,.scielo__floatingMenuJs2 .fm-button-close,.scielo__floatingMenuJs2 .fm-button-main{display:inline-block;position:relative;padding:0;color:#fff;cursor:pointer;outline:0;background-color:#3867ce;border:none;border-radius:50%;box-shadow:0 0 4px rgba(0,0,0,.14),0 4px 8px rgba(0,0,0,.28);-webkit-user-drag:none}.scielo__theme--dark .scielo__floatingMenuJs2 .fm-button-child,.scielo__theme--dark .scielo__floatingMenuJs2 .fm-button-close,.scielo__theme--dark .scielo__floatingMenuJs2 .fm-button-main{background:#86acff;color:#333}.scielo__theme--light .scielo__floatingMenuJs2 .fm-button-child,.scielo__theme--light .scielo__floatingMenuJs2 .fm-button-close,.scielo__theme--light .scielo__floatingMenuJs2 .fm-button-main{background-color:#3867ce;color:#fff}.scielo__floatingMenuJs2 .fm-button-close,.scielo__floatingMenuJs2 .fm-button-main{width:56px;height:56px;z-index:20;padding-top:17px;padding-left:16px}.scielo__floatingMenuJs2 .fm-button-close .glyphFloatMenu,.scielo__floatingMenuJs2 .fm-button-main .glyphFloatMenu{position:absolute;width:53px;height:56px;font-size:32px;line-height:56px;text-align:center}.scielo__floatingMenuJs2 .fm-button-close{background:#fff;color:#3867ce;display:none;z-index:999}.scielo__floatingMenuJs2 .fm-button-child{width:40px;height:40px;line-height:40px;text-align:center;-webkit-animation-fill-mode:both;-moz-animation-fill-mode:both;-ms-animation-fill-mode:both;-o-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.3s;-moz-animation-duration:.3s;-ms-animation-duration:.3s;-o-animation-duration:.3s;animation-duration:.3s;-o-transition:all .3s ease-out,text-indent .3s ease-out;-ms-transition:all .3s ease-out,text-indent .3s ease-out;-moz-transition:all .3s ease-out,text-indent .3s ease-out;-webkit-transition:all .3s ease-out,text-indent .3s ease-out;transition:all .3s ease-out,text-indent .3s ease-out;margin-top:1px;padding-top:6px}.scielo__floatingMenuJs2 .fm-button-child .glyphFloatMenu{font-size:24px}.scielo__floatingMenuJs2 .fm-button-child:hover{background-color:#3058af}.scielo__theme--dark .scielo__floatingMenuJs2 .fm-button-child:hover{background-color:#b6cdff;color:#333}.scielo__theme--light .scielo__floatingMenuJs2 .fm-button-child:hover{background-color:#3058af;color:#fff}.scielo__floatingMenuJs2 .fm-list-desktop,.scielo__floatingMenuJs2 .fm-list-mobile{position:absolute;bottom:42px;width:56px;min-height:56px;margin:0}.scielo__floatingMenuJs2 .fm-list-desktop li,.scielo__floatingMenuJs2 .fm-list-mobile li{position:absolute;box-sizing:border-box;padding:9px 0 2px 0;margin:0;width:50px;height:auto;list-style:none}.scielo__floatingMenuJs2 .fm-list-desktop li a,.scielo__floatingMenuJs2 .fm-list-mobile li a{display:none}.fm-list-mobile{padding:8px;margin-bottom:7px!important;margin-left:2px!important}.fm-list-mobile li{top:39px;left:6px}.fm-list-mobile li a:after{content:attr(data-mobile-tooltip);color:#fff;background:#333;position:absolute;margin-left:16px;display:inline-block;width:auto;height:auto;text-align:left;padding:5px;line-height:100%;border-radius:4px;font-size:.75rem;margin-top:3px}.scielo__theme--dark .fm-list-mobile li a:after{background:#fff;color:#333}.scielo__theme--light .fm-list-mobile li a:after{background:#333;color:#fff}.fm-list-mobile li a:before{content:'';border-right:4px solid #333;border-left:4px solid transparent;border-top:4px solid transparent;border-bottom:4px solid transparent;margin-left:13px;margin-top:10px;position:absolute;margin-left:32px}.scielo__theme--dark .fm-list-mobile li a:before{border-right:4px solid #fff}.scielo__theme--light .fm-list-mobile li a:before{border-right:4px solid #333}.fm-list-desktop{margin-left:15px!important}.fm-list-desktop li{display:inline;top:38px;left:6px;position:absolute;box-sizing:border-box;padding:2px 2px!important;margin:0;width:40px!important;height:auto;list-style:none;margin-top:10px!important;outline:0 solid red;left:0!important}.fm-list-desktop li a::after{content:'';display:none}.fm-list-desktop li a::before{content:'';display:none}.scielo__floatingMenuCttJs3{position:fixed;bottom:30px;z-index:5!important;width:auto;height:auto;margin:0}.scielo__floatingMenuCttJs3 .material-icons-outlined{vertical-align:baseline}.scielo__floatingMenuCttJs3>a{padding-top:6px}.scielo__floatingMenuJs3{transition:all .5s;box-sizing:border-box;z-index:1001;padding-left:0;white-space:nowrap;list-style:none;opacity:1;bottom:auto;opacity:1;margin:0;display:inline-block}.scielo__floatingMenuJs3 .fm-wrap{padding:0;margin:0}@media (min-width:576px){.scielo__floatingMenuJs3 .fm-wrap{padding:25px 25px 25px 0;margin:-25px -25px -25px 0}}.scielo__floatingMenuJs3 .fm-button-child,.scielo__floatingMenuJs3 .fm-button-close,.scielo__floatingMenuJs3 .fm-button-main{display:inline-block;position:relative;padding:0;color:#fff;cursor:pointer;outline:0;background-color:#3867ce;border:none;border-radius:50%;box-shadow:0 0 4px rgba(0,0,0,.14),0 4px 8px rgba(0,0,0,.28);-webkit-user-drag:none}.scielo__theme--dark .scielo__floatingMenuJs3 .fm-button-child,.scielo__theme--dark .scielo__floatingMenuJs3 .fm-button-close,.scielo__theme--dark .scielo__floatingMenuJs3 .fm-button-main{background:#86acff;color:#333}.scielo__theme--light .scielo__floatingMenuJs3 .fm-button-child,.scielo__theme--light .scielo__floatingMenuJs3 .fm-button-close,.scielo__theme--light .scielo__floatingMenuJs3 .fm-button-main{background-color:#3867ce;color:#fff}.scielo__floatingMenuJs3 .fm-button-close,.scielo__floatingMenuJs3 .fm-button-main{width:56px;height:56px;z-index:20;padding-top:17px;padding-left:16px}.scielo__floatingMenuJs3 .fm-button-close .glyphFloatMenu,.scielo__floatingMenuJs3 .fm-button-main .glyphFloatMenu{position:absolute;width:53px;height:56px;font-size:32px;line-height:56px;text-align:center}.scielo__floatingMenuJs3 .fm-button-close{background:#fff;color:#3867ce;display:none;z-index:999}.scielo__floatingMenuJs3 .fm-button-child{width:40px;height:40px;line-height:40px;text-align:center;-webkit-animation-fill-mode:both;-moz-animation-fill-mode:both;-ms-animation-fill-mode:both;-o-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.3s;-moz-animation-duration:.3s;-ms-animation-duration:.3s;-o-animation-duration:.3s;animation-duration:.3s;-o-transition:all .3s ease-out,text-indent .3s ease-out;-ms-transition:all .3s ease-out,text-indent .3s ease-out;-moz-transition:all .3s ease-out,text-indent .3s ease-out;-webkit-transition:all .3s ease-out,text-indent .3s ease-out;transition:all .3s ease-out,text-indent .3s ease-out;margin-top:1px;padding-top:6px}.scielo__floatingMenuJs3 .fm-button-child .glyphFloatMenu{font-size:24px}.scielo__floatingMenuJs3 .fm-button-child:hover{background-color:#3058af}.scielo__theme--dark .scielo__floatingMenuJs3 .fm-button-child:hover{background-color:#b6cdff;color:#333}.scielo__theme--light .scielo__floatingMenuJs3 .fm-button-child:hover{background-color:#3058af;color:#fff}.scielo__floatingMenuJs3 .fm-list-desktop,.scielo__floatingMenuJs3 .fm-list-mobile{position:absolute;bottom:42px;width:56px;min-height:56px;margin:0}.scielo__floatingMenuJs3 .fm-list-desktop li,.scielo__floatingMenuJs3 .fm-list-mobile li{position:absolute;box-sizing:border-box;padding:9px 0 2px 0;margin:0;width:50px;height:auto;list-style:none}.scielo__floatingMenuJs3 .fm-list-desktop li a,.scielo__floatingMenuJs3 .fm-list-mobile li a{display:none}.fm-list-mobile{padding:8px;margin-bottom:7px!important;margin-left:2px!important}.fm-list-mobile li{top:39px;left:6px}.fm-list-mobile li a:after{content:attr(data-mobile-tooltip);color:#fff;background:#333;position:absolute;margin-left:16px;display:inline-block;width:auto;height:auto;text-align:left;padding:5px;line-height:100%;border-radius:4px;font-size:.75rem;margin-top:3px}.scielo__theme--dark .fm-list-mobile li a:after{background:#fff;color:#333}.scielo__theme--light .fm-list-mobile li a:after{background:#333;color:#fff}.fm-list-mobile li a:before{content:'';border-right:4px solid #333;border-left:4px solid transparent;border-top:4px solid transparent;border-bottom:4px solid transparent;margin-left:13px;margin-top:10px;position:absolute;margin-left:32px}.scielo__theme--dark .fm-list-mobile li a:before{border-right:4px solid #fff}.scielo__theme--light .fm-list-mobile li a:before{border-right:4px solid #333}.fm-list-desktop{margin-left:15px!important}.fm-list-desktop li{display:inline;top:38px;left:6px;position:absolute;box-sizing:border-box;padding:2px 2px!important;margin:0;width:40px!important;height:auto;list-style:none;margin-top:10px!important;outline:0 solid red;left:0!important}.fm-list-desktop li a::after{content:'';display:none}.fm-list-desktop li a::before{content:'';display:none}@media (hover:none){.scielo__floatingMenuItem{display:none!important}.fm-list-mobile{display:block!important}}/*! nouislider - 14.1.1 - 12/15/2019 */.noUi-target,.noUi-target *{-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent;-webkit-user-select:none;-ms-touch-action:none;touch-action:none;-ms-user-select:none;-moz-user-select:none;user-select:none;-moz-box-sizing:border-box;box-sizing:border-box}.noUi-target{position:relative}.noUi-base,.noUi-connects{width:100%;height:100%;position:relative;z-index:1}.noUi-connects{overflow:hidden;z-index:0}.noUi-connect,.noUi-origin{will-change:transform;position:absolute;z-index:1;top:0;right:0;-ms-transform-origin:0 0;-webkit-transform-origin:0 0;-webkit-transform-style:preserve-3d;transform-origin:0 0;transform-style:flat}.noUi-connect{height:100%;width:100%}.noUi-origin{height:10%;width:10%}.noUi-txt-dir-rtl.noUi-horizontal .noUi-origin{left:0;right:auto}.noUi-vertical .noUi-origin{width:0}.noUi-horizontal .noUi-origin{height:0}.noUi-handle{-webkit-backface-visibility:hidden;backface-visibility:hidden;position:absolute}.noUi-touch-area{height:100%;width:100%}.noUi-state-tap .noUi-connect,.noUi-state-tap .noUi-origin{-webkit-transition:transform .3s;transition:transform .3s}.noUi-state-drag *{cursor:inherit!important}.noUi-horizontal{height:18px}.noUi-horizontal .noUi-handle{width:34px;height:28px;right:-17px;top:-6px}.noUi-vertical{width:18px}.noUi-vertical .noUi-handle{width:28px;height:34px;right:-6px;top:-17px}.noUi-txt-dir-rtl.noUi-horizontal .noUi-handle{left:-17px;right:auto}.noUi-target{background:#fafafa;border-radius:4px;border:1px solid #d3d3d3;box-shadow:inset 0 1px 1px #f0f0f0,0 3px 6px -5px #bbb}.noUi-connects{border-radius:3px}.noUi-connect{background:#3fb8af}.noUi-draggable{cursor:ew-resize}.noUi-vertical .noUi-draggable{cursor:ns-resize}.noUi-handle{border:1px solid #d9d9d9;border-radius:3px;background:#fff;cursor:default;box-shadow:inset 0 0 1px #fff,inset 0 1px 7px #ebebeb,0 3px 6px -3px #bbb}.noUi-active{box-shadow:inset 0 0 1px #fff,inset 0 1px 7px #ddd,0 3px 6px -3px #bbb}.noUi-handle:after,.noUi-handle:before{content:"";display:block;position:absolute;height:14px;width:1px;background:#e8e7e6;left:14px;top:6px}.noUi-handle:after{left:17px}.noUi-vertical .noUi-handle:after,.noUi-vertical .noUi-handle:before{width:14px;height:1px;left:6px;top:14px}.noUi-vertical .noUi-handle:after{top:17px}[disabled] .noUi-connect{background:#b8b8b8}[disabled] .noUi-handle,[disabled].noUi-handle,[disabled].noUi-target{cursor:not-allowed}.noUi-pips,.noUi-pips *{-moz-box-sizing:border-box;box-sizing:border-box}.noUi-pips{position:absolute;color:#999}.noUi-value{position:absolute;white-space:nowrap;text-align:center}.noUi-value-sub{color:#ccc;font-size:10px}.noUi-marker{position:absolute;background:#ccc}.noUi-marker-sub{background:#aaa}.noUi-marker-large{background:#aaa}.noUi-pips-horizontal{padding:10px 0;height:80px;top:100%;left:0;width:100%}.noUi-value-horizontal{-webkit-transform:translate(-50%,50%);transform:translate(-50%,50%)}.noUi-rtl .noUi-value-horizontal{-webkit-transform:translate(50%,50%);transform:translate(50%,50%)}.noUi-marker-horizontal.noUi-marker{margin-left:-1px;width:2px;height:5px}.noUi-marker-horizontal.noUi-marker-sub{height:10px}.noUi-marker-horizontal.noUi-marker-large{height:15px}.noUi-pips-vertical{padding:0 10px;height:100%;top:0;left:100%}.noUi-value-vertical{-webkit-transform:translate(0,-50%);transform:translate(0,-50%);padding-left:25px}.noUi-rtl .noUi-value-vertical{-webkit-transform:translate(0,50%);transform:translate(0,50%)}.noUi-marker-vertical.noUi-marker{width:5px;height:2px;margin-top:-1px}.noUi-marker-vertical.noUi-marker-sub{width:10px}.noUi-marker-vertical.noUi-marker-large{width:15px}.noUi-tooltip{display:block;position:absolute;border:1px solid #d9d9d9;border-radius:3px;background:#fff;color:#000;padding:5px;text-align:center;white-space:nowrap}.noUi-horizontal .noUi-tooltip{-webkit-transform:translate(-50%,0);transform:translate(-50%,0);left:50%;bottom:120%}.noUi-vertical .noUi-tooltip{-webkit-transform:translate(0,-50%);transform:translate(0,-50%);top:50%;right:120%}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(56,103,206,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(56,103,206,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none;background-color:#3867ce}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{transition:none}}.scielo__theme--dark .form-range::-webkit-slider-thumb{background-color:#86acff}.scielo__theme--light .form-range::-webkit-slider-thumb{background-color:#3867ce}.form-range::-webkit-slider-thumb:active{background-color:#3867ce}.scielo__theme--dark .form-range::-webkit-slider-thumb:active{background-color:#86acff}.scielo__theme--light .form-range::-webkit-slider-thumb:active{background-color:#3867ce}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;border-color:transparent;border-radius:1rem;background-color:#ccc}.scielo__theme--dark .form-range::-webkit-slider-runnable-track{background-color:rgba(255,255,255,.3)}.scielo__theme--light .form-range::-webkit-slider-runnable-track{background-color:#ccc}.form-range::-moz-range-thumb{width:1rem;height:1rem;border:0;border-radius:1rem;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;appearance:none;background-color:#3867ce}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{transition:none}}.scielo__theme--dark .form-range::-moz-range-thumb{background-color:#86acff}.scielo__theme--light .form-range::-moz-range-thumb{background-color:#3867ce}.form-range::-moz-range-thumb:active{background-color:#3867ce}.scielo__theme--dark .form-range::-moz-range-thumb:active{background-color:#86acff}.scielo__theme--light .form-range::-moz-range-thumb:active{background-color:#3867ce}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:rgba(0,0,0,.3);border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#ccc}.scielo__theme--dark .form-range:disabled::-webkit-slider-thumb{background-color:#717171}.scielo__theme--light .form-range:disabled::-webkit-slider-thumb{background-color:#ccc}.form-range:disabled::-moz-range-thumb{background-color:#ccc}.scielo__theme--dark .form-range:disabled::-moz-range-thumb{background-color:#717171}.scielo__theme--light .form-range:disabled::-moz-range-thumb{background-color:#ccc}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:rgba(0,0,0,.7);text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;text-decoration:none;background-color:#f7f6f4}.scielo__theme--dark .list-group-item-action:focus,.scielo__theme--dark .list-group-item-action:hover{background-color:#414141}.scielo__theme--light .list-group-item-action:focus,.scielo__theme--light .list-group-item-action:hover{background-color:#f7f6f4}.list-group-item-action:active{color:#393939;background-color:#efeeec}.list-group-item{position:relative;display:block;padding:.5rem 1rem;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:rgba(0,0,0,.6);pointer-events:none;background-color:#fff}.list-group-item:hover{background-color:#f7f6f4;text-decoration:none}.scielo__theme--dark .list-group-item:hover{background-color:#414141}.scielo__theme--light .list-group-item:hover{background-color:#f7f6f4}.list-group-item.active{z-index:2;color:#fff;background-color:#3867ce;border-color:#3867ce}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#223e7c;background-color:#d7e1f5}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#223e7c;background-color:#c2cbdd}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#223e7c;border-color:#223e7c}.list-group-item-secondary{color:#666;background-color:#fff}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#666;background-color:#e6e6e6}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#666;border-color:#666}.list-group-item-success{color:#1a5e29;background-color:#d5ebda}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#1a5e29;background-color:#c0d4c4}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#1a5e29;border-color:#1a5e29}.list-group-item-info{color:#145965;background-color:#d3eaee}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#145965;background-color:#bed3d6}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#145965;border-color:#145965}.list-group-item-warning{color:#6d4c00;background-color:#f0e5cc}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#6d4c00;background-color:#d8ceb8}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#6d4c00;border-color:#6d4c00}.list-group-item-danger{color:#720;background-color:#f4d7cc}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#720;background-color:#dcc2b8}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#720;border-color:#720}.list-group-item-light{color:#636262;background-color:#fdfdfd}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636262;background-color:#e4e4e4}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636262;border-color:#636262}.list-group-item-dark{color:#222;background-color:#d7d7d7}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#222;background-color:#c2c2c2}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#222;border-color:#222}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;background-clip:padding-box;border:1px solid rgba(0,0,0,.4);appearance:none;background-color:#fff;color:#393939;border-color:#ccc;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.scielo__theme--light .form-control{background-color:#fff;color:#333;border-color:#ccc}.scielo__theme--dark .form-control{background-color:#333;color:#c4c4c4;border-color:rgba(255,255,255,.3)}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{outline:0;background-color:#fff;color:#393939;border-color:rgba(56,103,206,.6);box-shadow:0 0 0 .25rem rgba(56,103,206,.25)}.scielo__theme--light .form-control:focus{background-color:#fff;color:#333;border-color:rgba(56,103,206,.6)}.scielo__theme--dark .form-control:focus{background-color:#333;color:#c4c4c4;border-color:rgba(134,172,255,.6)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::placeholder{color:rgba(0,0,0,.6);opacity:1}.scielo__theme--dark .form-control::placeholder{color:#adadad}.scielo__theme--light .form-control::placeholder{color:#6c6b6b}.form-control:disabled,.form-control[readonly]{opacity:1;pointer-events:auto;cursor:not-allowed;background-color:#efeeec;border-color:rgba(0,0,0,.396);color:rgba(0,0,0,.396)}.scielo__theme--dark .form-control:disabled,.scielo__theme--dark .form-control[readonly]{background-color:#414141;border-color:rgba(255,255,255,.2);color:rgba(255,255,255,.2)}.scielo__theme--light .form-control:disabled,.scielo__theme--light .form-control[readonly]{background-color:#efeeec;border-color:rgba(0,0,0,.396);color:rgba(0,0,0,.396)}.form-control:disabled::placeholder,.form-control[readonly]::placeholder{color:rgba(0,0,0,.396)}.scielo__theme--dark .form-control:disabled::placeholder,.scielo__theme--dark .form-control[readonly]::placeholder{color:rgba(255,255,255,.2)}.scielo__theme--light .form-control:disabled::placeholder,.scielo__theme--light .form-control[readonly]::placeholder{color:rgba(0,0,0,.396)}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;margin-inline-end:.75rem;color:#393939;background-color:#efeeec;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;height:48px;background-color:#efeeec;color:#333;border-color:#ccc}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.scielo__theme--light .form-control::file-selector-button{background-color:#efeeec;color:#333;border-color:#ccc}.scielo__theme--dark .form-control::file-selector-button{background-color:#414141;color:#c4c4c4;border-color:rgba(255,255,255,.3)}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#e3e2e0}.scielo__theme--dark .form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#3b3b3b}.scielo__theme--light .form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#e3e2e0}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;margin-inline-end:.75rem;color:#393939;background-color:#efeeec;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:all .8s}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#d9d9d9}.scielo__theme--dark .form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dcdcdc;color:#333}.scielo__theme--light .form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#d9d9d9;color:#333}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;background-color:transparent;border:solid transparent;border-width:1px 0;color:#6c6b6b;outline:0}.scielo__theme--dark .form-control-plaintext{color:#adadad}.scielo__theme--light .form-control-plaintext{color:#6c6b6b}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.12 .5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.5rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;margin-inline-end:1rem}textarea.form-control{min-height:5rem;height:5rem}.scielo__search-articles textarea.form-control{min-height:calc(1.5em + .75rem + 2px);height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{max-width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 1rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid rgba(0,0,0,.4);border-radius:.25rem;appearance:none;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23414141' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-color:#fff;color:#393939;border-color:#ccc}.scielo__theme--light .form-select{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23414141' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-color:#fff;color:#333;border-color:#ccc}.scielo__theme--dark .form-select{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23C4C4C4' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-color:#333;color:#c4c4c4;border-color:rgba(255,255,255,.3)}.form-select:focus{outline:0;box-shadow:0 0 0 .25rem rgba(56,103,206,.25);border-color:rgba(56,103,206,.6)}.scielo__theme--dark .form-select:focus{border-color:rgba(134,172,255,.6)}.scielo__theme--light .form-select:focus{border-color:rgba(56,103,206,.6)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{pointer-events:auto;cursor:not-allowed;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%280, 0, 0, 0.396%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-color:#efeeec;border-color:rgba(0,0,0,.396);color:rgba(0,0,0,.396)}.scielo__theme--dark .form-select:disabled{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%28255, 255, 255, 0.2%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-color:#414141;border-color:rgba(255,255,255,.2);color:rgba(255,255,255,.2)}.scielo__theme--light .form-select:disabled{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%280, 0, 0, 0.396%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-color:#efeeec;border-color:rgba(0,0,0,.396);color:rgba(0,0,0,.396)}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #393939}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-repeat:no-repeat;background-position:center;background-size:contain;appearance:none;color-adjust:exact;transition:background-color .15s ease-in-out,background-position .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;border-color:#ccc;background-color:#fff}@media (prefers-reduced-motion:reduce){.form-check-input{transition:none}}.scielo__theme--dark .form-check-input{border-color:rgba(255,255,255,.3);background-color:#333}.scielo__theme--light .form-check-input{border-color:#ccc;background-color:#fff}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{outline:0;box-shadow:0 0 0 .25rem rgba(56,103,206,.25);border-color:#ccc}.scielo__theme--dark .form-check-input:focus{border-color:rgba(255,255,255,.3)}.scielo__theme--light .form-check-input:focus{border-color:#ccc}.form-check-input:checked{background-color:#3867ce;border-color:#3867ce}.scielo__theme--dark .form-check-input:checked{background-color:#86acff;border-color:#86acff}.scielo__theme--light .form-check-input:checked{background-color:#3867ce;border-color:#3867ce}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.scielo__theme--dark .form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23333' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.scielo__theme--light .form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.scielo__theme--dark .form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23333'/%3e%3c/svg%3e")}.scielo__theme--light .form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#3867ce;border-color:#3867ce;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.scielo__theme--dark .form-check-input[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23333' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.scielo__theme--light .form-check-input[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23ccc'/%3e%3c/svg%3e");background-color:#fff;border-color:#ccc}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.scielo__theme--dark .form-switch .form-check-input{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.3%29'/%3e%3c/svg%3e");background-color:#333;border-color:rgba(255,255,255,.3)}.scielo__theme--light .form-switch .form-check-input{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23ccc'/%3e%3c/svg%3e");background-color:#fff;border-color:#ccc}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23ccc'/%3e%3c/svg%3e")}.scielo__theme--dark .form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.3%29'/%3e%3c/svg%3e")}.scielo__theme--light .form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23ccc'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-color:#3867ce;border-color:#3867ce;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.scielo__theme--dark .form-switch .form-check-input:checked{background-color:#86acff;border-color:#86acff}.scielo__theme--light .form-switch .form-check-input:checked{background-color:#3867ce;border-color:#3867ce}.scielo__theme--dark .form-switch .form-check-input:checked{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23333'/%3e%3c/svg%3e")}.scielo__theme--light .form-switch .form-check-input:checked{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.scielo__form-file{position:relative;height:3rem;overflow:hidden}.scielo__form-file input{-webkit-appearance:none;appearance:none;position:absolute;transform:translateY(-500%);top:0;width:auto}.scielo__form-file:after{content:b3__ico--char(attach_file);position:absolute;top:50%;right:.75rem;transform:translateY(-50%);font-family:b3-icons;color:#fff;font-size:1.5rem;pointer-events:none}.scielo__theme--light .scielo__form-file:after{color:#fff}.scielo__theme--dark .scielo__form-file:after{color:#eee}.scielo__form-file label{height:3rem;left:0;width:100%;top:0;transform:translateY(0);padding:0 3rem 0 .75rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer;border-bottom:1px solid rgba(56,103,206,.25);line-height:3rem;pointer-events:all}.scielo__form-file.has-placeholder:not(.small)>label,.scielo__form-file.has-value:not(.small)>label,.scielo__form-file.is-focused:not(.small)>label{font-weight:400;font-size:1rem;line-height:1.2em;line-height:3rem;top:0;color:#333}.scielo__theme--dark .scielo__form-file.has-placeholder:not(.small)>label,.scielo__theme--dark .scielo__form-file.has-value:not(.small)>label,.scielo__theme--dark .scielo__form-file.is-focused:not(.small)>label{color:#c4c4c4}.scielo__theme--light .scielo__form-file.has-placeholder:not(.small)>label,.scielo__theme--light .scielo__form-file.has-value:not(.small)>label,.scielo__theme--light .scielo__form-file.is-focused:not(.small)>label{color:#333}.input-group{border-radius:3px;flex-flow:row nowrap;height:3rem}.input-group.is-search{border-radius:3rem}.input-group-text{border-radius:3px;font-weight:400;font-size:1rem;line-height:1.2em;padding-top:0;padding-bottom:0;color:#333;background-color:#efeeec;border-color:#ccc;color:#333}.scielo__theme--dark .input-group-text{color:#c4c4c4}.scielo__theme--light .input-group-text{color:#333}.scielo__theme--dark .input-group-text{background-color:#414141;border-color:rgba(255,255,255,.3);color:#c4c4c4}.scielo__theme--light .input-group-text{background-color:#efeeec;border-color:#ccc;color:#333}.input-group-text span[class^=b3__ico--]{font-size:1.5rem}.input-group-text .scielo__form-checkbox~label,.input-group-text .scielo__form-radio~label{margin-left:0;margin-right:0}.input-group .scielo__form-control input,.input-group .scielo__form-control label,.input-group .scielo__form-control select,.input-group .scielo__form-control textarea,.input-group .scielo__form-file input,.input-group .scielo__form-file label,.input-group .scielo__form-file select,.input-group .scielo__form-file textarea,.input-group .scielo__form-select input,.input-group .scielo__form-select label,.input-group .scielo__form-select select,.input-group .scielo__form-select textarea{border-bottom:none}.input-group .scielo__form-control:first-child,.input-group .scielo__form-file:first-child,.input-group .scielo__form-select:first-child{border-top-right-radius:0;border-bottom-right-radius:0}.input-group .scielo__form-control:last-child,.input-group .scielo__form-file:last-child,.input-group .scielo__form-select:last-child{border-top-left-radius:0;border-bottom-left-radius:0}.input-group .scielo__form-control,.input-group .scielo__form-file,.input-group .scielo__form-select{flex:1 1 auto}.input-group .btn{margin-bottom:0;padding:.75rem 1.2rem;border-radius:.25rem;line-height:1.5rem;height:3rem;padding-left:1.5rem;padding-right:1.5rem}.input-group .btn.dropdown-toggle:not(.dropdown-toggle-split){padding-right:2.5rem}.input-group .btn.dropdown-toggle:after{right:.9375rem;width:1.25rem;height:1.25rem;font-size:1.25rem;line-height:1.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.5rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.12 .5rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:1.75rem}.picker{font-size:16px;text-align:left;line-height:1.2;color:#000;position:absolute;z-index:10000;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;outline:0}.picker__input{cursor:default}.picker__input.picker__input--active{border-color:#0089ec}.picker__holder{width:100%;overflow-y:auto;-webkit-overflow-scrolling:touch}/*! + * Default mobile-first, responsive styling for pickadate.js + * Demo: http://amsul.github.io/pickadate.js + */.picker__frame,.picker__holder{top:0;bottom:0;left:0;right:0;-ms-transform:translateY(100%);transform:translateY(100%)}.picker__holder{position:fixed;transition:background .15s ease-out,transform 0s .15s;-webkit-backface-visibility:hidden}.picker__frame{position:absolute;margin:0 auto;min-width:256px;max-width:666px;width:100%;-moz-opacity:0;opacity:0;transition:all .15s ease-out}@media (min-height:33.875em){.picker__frame{overflow:visible;top:auto;bottom:-100%;max-height:80%}}@media (min-height:40.125em){.picker__frame{margin-bottom:7.5%}}.picker__wrap{display:table;width:100%;height:100%}@media (min-height:33.875em){.picker__wrap{display:block}}.picker__box{background:#fff;display:table-cell;vertical-align:middle}@media (min-height:26.5em){.picker__box{font-size:1.25em}}@media (min-height:33.875em){.picker__box{display:block;font-size:1.33em;border:1px solid #777;border-top-color:#898989;border-bottom-width:0;border-radius:5px 5px 0 0;box-shadow:0 12px 36px 16px rgba(0,0,0,.24)}}@media (min-height:40.125em){.picker__box{font-size:1.5em;border-bottom-width:1px;border-radius:5px}}.picker--opened .picker__holder{-ms-transform:translateY(0);transform:translateY(0);background:0 0;zoom:1;background:rgba(0,0,0,.32);transition:background .15s ease-out}.picker--opened .picker__frame{-ms-transform:translateY(0);transform:translateY(0);-moz-opacity:1;opacity:1}@media (min-height:33.875em){.picker--opened .picker__frame{top:auto;bottom:0}}.picker__box{padding:0 1em}.picker__header{text-align:center;position:relative;margin-top:.75em}.picker__month,.picker__year{font-weight:500;display:inline-block;margin-left:.25em;margin-right:.25em}.picker__year{color:#999;font-size:.8em;font-style:italic}.picker__select--month,.picker__select--year{border:1px solid #b7b7b7;height:2em;padding:.5em;margin-left:.25em;margin-right:.25em}@media (min-width:24.5em){.picker__select--month,.picker__select--year{margin-top:-.5em}}.picker__select--month{width:35%}.picker__select--year{width:22.5%}.picker__select--month:focus,.picker__select--year:focus{border-color:#0089ec}.picker__nav--next,.picker__nav--prev{position:absolute;padding:.5em 1.25em;width:1em;height:1em;box-sizing:content-box;top:-.25em}@media (min-width:24.5em){.picker__nav--next,.picker__nav--prev{top:-.33em}}.picker__nav--prev{left:-1em;padding-right:1.25em}@media (min-width:24.5em){.picker__nav--prev{padding-right:1.5em}}.picker__nav--next{right:-1em;padding-left:1.25em}@media (min-width:24.5em){.picker__nav--next{padding-left:1.5em}}.picker__nav--next:before,.picker__nav--prev:before{content:" ";border-top:.5em solid transparent;border-bottom:.5em solid transparent;border-right:.75em solid #000;width:0;height:0;display:block;margin:0 auto}.picker__nav--next:before{border-right:0;border-left:.75em solid #000}.picker__nav--next:hover,.picker__nav--prev:hover{cursor:pointer;color:#000;background:#b1dcfb}.picker__nav--disabled,.picker__nav--disabled:before,.picker__nav--disabled:before:hover,.picker__nav--disabled:hover{cursor:default;background:0 0;border-right-color:#f5f5f5;border-left-color:#f5f5f5}.picker__table{text-align:center;border-collapse:collapse;border-spacing:0;table-layout:fixed;font-size:inherit;width:100%;margin-top:.75em;margin-bottom:.5em}@media (min-height:33.875em){.picker__table{margin-bottom:.75em}}.picker__table td{margin:0;padding:0}.picker__weekday{width:14.285714286%;font-size:.75em;padding-bottom:.25em;color:#999;font-weight:500}@media (min-height:33.875em){.picker__weekday{padding-bottom:.5em}}.picker__day{padding:.3125em 0;font-weight:200;border:1px solid transparent}.picker__day--today{position:relative}.picker__day--today:before{content:" ";position:absolute;top:2px;right:2px;width:0;height:0;border-top:.5em solid #0059bc;border-left:.5em solid transparent}.picker__day--disabled:before{border-top-color:#aaa}.picker__day--outfocus{color:#ddd}.picker__day--infocus:hover,.picker__day--outfocus:hover{cursor:pointer;color:#000;background:#b1dcfb}.picker__day--highlighted{border-color:#0089ec}.picker--focused .picker__day--highlighted,.picker__day--highlighted:hover{cursor:pointer;color:#000;background:#b1dcfb}.picker--focused .picker__day--selected,.picker__day--selected,.picker__day--selected:hover{background:#0089ec;color:#fff}.picker--focused .picker__day--disabled,.picker__day--disabled,.picker__day--disabled:hover{background:#f5f5f5;border-color:#f5f5f5;color:#ddd;cursor:default}.picker__day--highlighted.picker__day--disabled,.picker__day--highlighted.picker__day--disabled:hover{background:#bbb}.picker__footer{text-align:center}.picker__button--clear,.picker__button--close,.picker__button--today{border:1px solid #fff;background:#fff;font-size:.8em;padding:.66em 0;font-weight:700;width:33%;display:inline-block;vertical-align:bottom}.picker__button--clear:hover,.picker__button--close:hover,.picker__button--today:hover{cursor:pointer;color:#000;background:#b1dcfb;border-bottom-color:#b1dcfb}.picker__button--clear:focus,.picker__button--close:focus,.picker__button--today:focus{background:#b1dcfb;border-color:#0089ec;outline:0}.picker__button--clear:before,.picker__button--close:before,.picker__button--today:before{position:relative;display:inline-block;height:0}.picker__button--clear:before,.picker__button--today:before{content:" ";margin-right:.45em}.picker__button--today:before{top:-.05em;width:0;border-top:.66em solid #0059bc;border-left:.66em solid transparent}.picker__button--clear:before{top:-.25em;width:.66em;border-top:3px solid #e20}.picker__button--close:before{content:"\D7";top:-.1em;vertical-align:top;font-size:1.1em;margin-right:.35em;color:#777}.picker__button--today[disabled],.picker__button--today[disabled]:hover{background:#f5f5f5;border-color:#f5f5f5;color:#ddd;cursor:default}.picker__button--today[disabled]:before{border-top-color:#aaa}.picker--opened .picker__holder{background:rgba(0,0,0,.8)}.picker__box{padding:.625rem 1.0625rem;border:0;box-shadow:none;border-radius:6px}.picker__nav--next,.picker__nav--prev{color:#fff}.picker__nav--next:before,.picker__nav--prev:before{font-family:b3-icons;position:absolute;border:0;font-size:1.5rem}.picker__nav--next:hover,.picker__nav--prev:hover{background:0 0}.picker__nav--next:hover:before,.picker__nav--prev:hover:before{color:rgba(56,103,206,.25)}.picker__nav--prev:before{content:b3__ico--char(keyboard_arrow_left)}.picker__nav--next:before{content:b3__ico--char(keyboard_arrow_right)}.picker__weekday{width:auto;color:#333;font-weight:400;font-size:1rem;line-height:1.2em}.picker__header{margin-top:0;padding-top:.3125rem}.picker__header,.picker__year{font-size:1.03125rem;letter-spacing:0}.picker__year{color:#333;font-style:normal}.picker__day,.picker__day--infocus,.picker__day--outfocus,.picker__day--today{padding:0;margin:0 auto;border-radius:99px;width:1.875rem;height:1.875rem;text-align:center;line-height:1.75rem;font-size:.84375rem;color:#3867ce;font-weight:400;background:0 0;border:2px solid transparent}.picker__day--today:before{display:none}.picker__day--infocus:hover,.picker__day--outfocus:hover{color:#3867ce;background:rgba(0,176,230,.08);border-color:rgba(0,176,230,0)}.picker__day--outfocus{color:rgba(0,0,0,.396)}.picker--focused .picker__day--highlighted,.picker__day--highlighted:hover{color:#333;border-color:rgba(56,103,206,.25);background:0 0}.picker__button--clear,.picker__button--close,.picker__button--today{padding:0;text-transform:uppercase;border:0;font-weight:400!important;font-size:1rem;letter-spacing:.1px!important;white-space:nowrap}.picker__button--clear:before,.picker__button--close:before,.picker__button--today:before{display:none}.picker__button--clear:hover,.picker__button--close:hover,.picker__button--today:hover{background:0 0;border:none;font-weight:400!important;font-size:1rem;letter-spacing:.1px!important;white-space:nowrap}.picker__button--clear{color:#00314c}.picker__button--clear:hover{color:#333}.picker__button--close{color:#c63800}.picker__button--close:hover{color:#ff7e4a}.picker__button--today{color:#3867ce}.picker__button--today:hover{color:rgba(56,103,206,.25)}.scielo__menu{position:relative;display:inline-block;width:40px;height:20px;margin:5px 0;margin-left:8px;z-index:100;outline:0;-webkit-animation-fill-mode:both;-moz-animation-fill-mode:both;-ms-animation-fill-mode:both;-o-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.2s;-moz-animation-duration:.2s;-ms-animation-duration:.2s;-o-animation-duration:.2s;animation-duration:.2s;-o-transition:all .2s ease-out,text-indent .2s ease-out;-ms-transition:all .2s ease-out,text-indent .2s ease-out;-moz-transition:all .2s ease-out,text-indent .2s ease-out;-webkit-transition:all .2s ease-out,text-indent .2s ease-out;transition:all .2s ease-out,text-indent .2s ease-out}.scielo__menu:active,.scielo__menu:focus{outline:0}.scielo__menu .material-icons-outlined{color:#333}.scielo__theme--dark .scielo__menu .material-icons-outlined{color:#c4c4c4}.scielo__theme--light .scielo__menu .material-icons-outlined{color:#333}.scielo__menu.opened{margin-left:270px}.scielo__mainMenu{position:absolute;top:-1000px;z-index:99;width:300px;background:#fff;border:1px solid #ccc;border-top:0;border-radius:4px;border-top-left-radius:0;border-top-right-radius:0;padding:35px 20px 0 20px;padding:0;box-shadow:0 0 7px rgba(0,0,0,.1);font-size:.85em}.scielo__theme--dark .scielo__mainMenu{background:#333;border-color:rgba(255,255,255,.3)}.scielo__theme--light .scielo__mainMenu{background:#fff;border-color:#ccc}.scielo__mainMenu .logo-svg{background:url(../img/logo-scielo-no-label.svg);background-position:center center;background-repeat:no-repeat;display:block;width:100px;height:100px}.scielo__theme--dark .scielo__mainMenu .logo-svg{background-image:url(../img/logo-scielo-no-label-negative.svg)}.scielo__theme--light .scielo__mainMenu .logo-svg{background-image:url(../img/logo-scielo-no-label.svg)}.scielo__mainMenu ul{margin:0;padding:0}.scielo__mainMenu li{list-style:none;border-bottom:1px dotted #6c6b6b;padding-bottom:7px}.scielo__theme--dark .scielo__mainMenu li{border-color:rgba(255,255,255,.3)}.scielo__theme--light .scielo__mainMenu li{border-color:#ccc}.scielo__mainMenu li:last-child{border-bottom:0}.scielo__mainMenu li a,.scielo__mainMenu li strong{display:block;color:#00314c;text-decoration:none}.scielo__theme--dark .scielo__mainMenu li a,.scielo__theme--dark .scielo__mainMenu li strong{color:#eee}.scielo__theme--light .scielo__mainMenu li a,.scielo__theme--light .scielo__mainMenu li strong{color:#00314c}.scielo__mainMenu li a:hover,.scielo__mainMenu li strong:hover{color:#3867ce}.scielo__theme--dark .scielo__mainMenu li a:hover,.scielo__theme--dark .scielo__mainMenu li strong:hover{color:#86acff}.scielo__theme--light .scielo__mainMenu li a:hover,.scielo__theme--light .scielo__mainMenu li strong:hover{color:#3867ce}.scielo__mainMenu li a{padding:.4rem 1rem}.scielo__mainMenu li a:hover{background-color:#f7f6f4}.scielo__mainMenu li li{background:0 0;padding-bottom:0;border-bottom:0}.scielo-ico-menu{display:inline-block}.scielo-ico-menu-opened{display:none}.scielo__accessibleMenu{position:absolute;z-index:9;top:20px}.scielo__accessibleMenu summary{cursor:pointer;font-size:1.2rem;background:#fff;color:#333;padding:2px 4px;border-radius:5px;display:flex;align-items:center;gap:10px;width:110px;min-height:32px;justify-content:center;transition:background .3s ease-in-out;border:1px solid #ccc}@media (max-width:767.98px){.scielo__accessibleMenu summary{width:70px}.scielo__accessibleMenu summary span:not(.menu-icon){display:none}}.scielo__accessibleMenu summary span{font-size:1rem}.scielo__accessibleMenu summary .menu-icon{display:inline-block;width:16px;height:2px;background:#333;position:relative;transition:transform .3s ease-in-out}.scielo__accessibleMenu summary .menu-icon::after,.scielo__accessibleMenu summary .menu-icon::before{content:"";width:16px;height:2px;background:#333;position:absolute;left:0;transition:transform .3s ease-in-out}.scielo__accessibleMenu summary .menu-icon::before{top:-6px}.scielo__accessibleMenu summary .menu-icon::after{top:6px}@media (max-width:767.98px){.scielo__accessibleMenu[open]{position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:1000;overflow:hidden}.scielo__accessibleMenu[open] summary{position:fixed;top:0;left:0;width:100%;border-radius:0;padding:12px 0}}@media (max-width:767.98px) and (max-width:767.98px){.scielo__accessibleMenu[open] summary span:not(.menu-icon){display:inline-block}}@media (max-width:767.98px){.scielo__accessibleMenu[open] nav{position:absolute;top:47px;left:0;width:100%;height:calc(100vh - 47px);overflow-y:auto;padding:1rem;border-radius:0}}.scielo__accessibleMenu[open] summary{background-color:#f7f6f4}.scielo__accessibleMenu[open] .menu-icon{background:0 0}.scielo__accessibleMenu[open] .menu-icon::before{transform:rotate(45deg);top:0}.scielo__accessibleMenu[open] .menu-icon::after{transform:rotate(-45deg);top:0}.scielo__accessibleMenu[open] nav{opacity:1;max-height:750px}.scielo__accessibleMenu nav{background:#fff;padding:.5rem 0;margin-top:2px;width:280px;border-radius:5px;border:1px solid #ccc;opacity:0;max-height:0;overflow:hidden;transition:opacity .3s ease-in-out,max-height .3s ease-in-out;box-shadow:0 0 7px rgba(0,0,0,.1)}.scielo__accessibleMenu nav ul{list-style:none;padding:0;margin:0}.scielo__accessibleMenu nav ul li{margin:5px 0}.scielo__accessibleMenu nav ul li.logo{text-align:center;padding-top:1rem}.scielo__accessibleMenu nav ul li a{text-decoration:none;padding:.25rem 1rem;display:block;color:#00314c}.scielo__accessibleMenu nav ul li a:focus-visible,.scielo__accessibleMenu nav ul li a:hover{background-color:#f7f6f4;color:#3867ce}.scielo__accessibleMenu nav ul li a:focus-visible strong,.scielo__accessibleMenu nav ul li a:hover strong{color:#3867ce}.scielo__accessibleMenu nav ul li a strong{color:#00314c}ul.scielo__menu-contexto,ul.scielo__menu-contexto ul{list-style:none}ul.scielo__menu-contexto ul{margin-bottom:1rem}ul.scielo__menu-contexto .nav-link{padding:0;color:#6c6b6b}.sticky-top{top:80px}.bd-example .h4,.bd-example .h5,.bd-example h4,.bd-example h5{margin-top:3rem;scroll-margin-top:5.625rem}.bd-example hr{margin-top:3rem}.bd-example ul{margin-bottom:3rem}.scielo__menu-responsivo{position:relative;display:inline-block;width:40px;height:20px;margin:5px 0;margin-left:8px;z-index:100;outline:0;-webkit-animation-fill-mode:both;-moz-animation-fill-mode:both;-ms-animation-fill-mode:both;-o-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.2s;-moz-animation-duration:.2s;-ms-animation-duration:.2s;-o-animation-duration:.2s;animation-duration:.2s;-o-transition:all .2s ease-out,text-indent .2s ease-out;-ms-transition:all .2s ease-out,text-indent .2s ease-out;-moz-transition:all .2s ease-out,text-indent .2s ease-out;-webkit-transition:all .2s ease-out,text-indent .2s ease-out;transition:all .2s ease-out,text-indent .2s ease-out}.scielo__menu-responsivo:active,.scielo__menu-responsivo:focus{outline:0}.scielo__menu-responsivo .material-icons-outlined{color:#333}.scielo__theme--dark .scielo__menu-responsivo .material-icons-outlined{color:#c4c4c4}.scielo__theme--light .scielo__menu-responsivo .material-icons-outlined{color:#333}.scielo__menu-responsivo.opened{margin-left:270px}.scielo__mainMenu-responsivo,.touch-side-swipe{position:absolute;top:-1000px;z-index:99;width:300px;background:#fff;border:1px solid #ccc;border-top:0;border-radius:4px;border-top-left-radius:0;border-top-right-radius:0;padding:2rem 20px 0 20px;box-shadow:0 0 7px rgba(0,0,0,.1)}.scielo__theme--dark .scielo__mainMenu-responsivo,.scielo__theme--dark .touch-side-swipe{background:#333;border-color:rgba(255,255,255,.3)}.scielo__theme--light .scielo__mainMenu-responsivo,.scielo__theme--light .touch-side-swipe{background:#fff;border-color:#ccc}.scielo__mainMenu-responsivo .logo-svg,.touch-side-swipe .logo-svg{background:url(../img/logo-scielo-no-label.svg);background-position:center center;background-repeat:no-repeat;display:block;width:100px;height:100px}.scielo__theme--dark .scielo__mainMenu-responsivo .logo-svg,.scielo__theme--dark .touch-side-swipe .logo-svg{background-image:url(../img/logo-scielo-no-label-negative.svg)}.scielo__theme--light .scielo__mainMenu-responsivo .logo-svg,.scielo__theme--light .touch-side-swipe .logo-svg{background-image:url(../img/logo-scielo-no-label.svg)}.scielo__mainMenu-responsivo ul,.touch-side-swipe ul{margin:0;padding:0}.scielo__mainMenu-responsivo li,.touch-side-swipe li{list-style:none;border-bottom:1px dotted #6c6b6b;padding-bottom:7px}.scielo__theme--dark .scielo__mainMenu-responsivo li,.scielo__theme--dark .touch-side-swipe li{border-color:rgba(255,255,255,.3)}.scielo__theme--light .scielo__mainMenu-responsivo li,.scielo__theme--light .touch-side-swipe li{border-color:#ccc}.scielo__mainMenu-responsivo li:last-child,.touch-side-swipe li:last-child{border-bottom:0}.scielo__mainMenu-responsivo li a,.scielo__mainMenu-responsivo li strong,.touch-side-swipe li a,.touch-side-swipe li strong{display:block;color:#00314c;text-decoration:none}.scielo__theme--dark .scielo__mainMenu-responsivo li a,.scielo__theme--dark .scielo__mainMenu-responsivo li strong,.scielo__theme--dark .touch-side-swipe li a,.scielo__theme--dark .touch-side-swipe li strong{color:#eee}.scielo__theme--light .scielo__mainMenu-responsivo li a,.scielo__theme--light .scielo__mainMenu-responsivo li strong,.scielo__theme--light .touch-side-swipe li a,.scielo__theme--light .touch-side-swipe li strong{color:#00314c}.scielo__mainMenu-responsivo li a:hover,.scielo__mainMenu-responsivo li strong:hover,.touch-side-swipe li a:hover,.touch-side-swipe li strong:hover{color:#3867ce}.scielo__theme--dark .scielo__mainMenu-responsivo li a:hover,.scielo__theme--dark .scielo__mainMenu-responsivo li strong:hover,.scielo__theme--dark .touch-side-swipe li a:hover,.scielo__theme--dark .touch-side-swipe li strong:hover{color:#86acff}.scielo__theme--light .scielo__mainMenu-responsivo li a:hover,.scielo__theme--light .scielo__mainMenu-responsivo li strong:hover,.scielo__theme--light .touch-side-swipe li a:hover,.scielo__theme--light .touch-side-swipe li strong:hover{color:#3867ce}.scielo__mainMenu-responsivo li a,.touch-side-swipe li a{padding:.4rem 1rem}.scielo__mainMenu-responsivo li a:hover,.touch-side-swipe li a:hover{background-color:#f7f6f4}.scielo__mainMenu-responsivo li li,.touch-side-swipe li li{background:0 0;padding-bottom:0;border-bottom:0}.scielo-ico-menu{display:inline-block}.scielo-ico-menu-opened{display:none}.touch-side-swipe{display:none;height:100%;width:100%;top:0;left:0}.tss .touch-side-swipe{display:block;overflow-y:overlay}.tss{z-index:9999;position:fixed;top:0;left:0;height:100%;will-change:transform;transition-property:transform;transition-timing-function:ease}.tss-wrap{height:100%;width:100%;position:absolute;top:0;left:0}.tss-label{z-index:99999;position:absolute;top:5px;right:-44px;width:44px;height:44px;display:block;cursor:pointer}.tss-label_pic{position:relative;display:inline-block;vertical-align:middle;font-style:normal;text-align:left;text-indent:-9999px;direction:ltr;box-sizing:border-box;transition:transform .2s ease}.tss-label_pic:after,.tss-label_pic:before{content:'';pointer-events:none;transition:transform .2s ease}.tss--close .tss-label_pic{color:#000;width:30px;height:4px;box-shadow:inset 0 0 0 32px,0 -8px,0 8px;margin:15px 7px}.tss--close .tss-label_pic:after{position:absolute;transform:translateY(4px);color:#fff;width:30px;height:3px;box-shadow:inset 0 0 0 32px,0 -8px,0 8px;top:0;left:0}.tss--open .tss-label_pic{color:#fff;padding:0;width:40px;height:40px;margin:2px;transform:rotate(45deg)}.tss--open .tss-label_pic:before{width:40px;height:2px}.tss--open .tss-label_pic:after{width:2px;height:40px}.tss--open .tss-label_pic:after,.tss--open .tss-label_pic:before{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);box-shadow:inset 0 0 0 32px}.tss-bg{background:#000;position:fixed;width:100%;height:100%;top:0;left:0;opacity:0;will-change:opacity;transition-property:opacity;transition-timing-function:ease}.touch-side-swipe{background:#fff}header{background:#fff}.page-item{background:0 0;border-color:rgba(0,0,0,.3)}.page-item.active .page-link{background:0 0;border-color:#3867ce;background-color:#3867ce;color:#fff;font-weight:400}.scielo__theme--dark .page-item.active .page-link{background-color:#86acff;border-color:#86acff;color:#eee}.scielo__theme--light .page-item.active .page-link{border-color:#3867ce;background-color:#3867ce;color:#fff}.page-item.disabled .page-link{background:0 0;color:rgba(0,0,0,.396);border-color:rgba(0,0,0,.396);cursor:not-allowed}.scielo__theme--dark .page-item.disabled .page-link{color:rgba(255,255,255,.2);border-color:rgba(255,255,255,.2)}.scielo__theme--light .page-item.disabled .page-link{color:rgba(0,0,0,.396);border-color:rgba(0,0,0,.396)}.page-item .material-icons,.page-item .material-icons-outlined{font-size:1rem;line-height:1.2em}.page-link{background:0 0;font-weight:400;font-size:1rem;line-height:1.2em;height:2rem;text-align:center;border-color:#ccc;transition:all .2s;color:#3867ce;font-weight:400}.scielo__theme--dark .page-link{color:#86acff;border-color:rgba(255,255,255,.3);font-weight:400}.scielo__theme--light .page-link{color:#3867ce;border-color:#ccc;font-weight:400}.page-link:hover{background:#f7f6f4;color:#3867ce;border-color:#ccc}.scielo__theme--dark .page-link:hover{background:#414141;color:#86acff;border-color:rgba(255,255,255,.3)}.scielo__theme--light .page-link:hover{background:#f7f6f4;color:#3867ce;border-color:#ccc}.scielo__ico:before{content:'';position:absolute;font-size:1.5rem;line-height:1.5rem}.scielo__ico--first_page:before{font-family:'Material Icons Outlined';content:"first_page";color:inherit}.scielo__ico--last_page:before{font-family:'Material Icons Outlined';content:"last_page";color:inherit}.scielo__ico--navigate_before:before{font-family:'Material Icons Outlined';content:"navigate_before";color:inherit}.scielo__ico--navigate_next:before{font-family:'Material Icons Outlined';content:"navigate_next";color:inherit}.breadcrumb{background:#efeeec;padding:1.125rem;border-radius:.25rem}.scielo__theme--dark .breadcrumb{background:#414141}.scielo__theme--light .breadcrumb{background:#efeeec}.breadcrumb li a{font-weight:400!important}.breadcrumb li.breadcrumb-item+.breadcrumb-item:before{color:rgba(0,0,0,.396)}.scielo__theme--dark .breadcrumb li.breadcrumb-item+.breadcrumb-item:before{color:rgba(255,255,255,.2)}.scielo__theme--light .breadcrumb li.breadcrumb-item+.breadcrumb-item:before{color:rgba(0,0,0,.396)}.breadcrumb li.active{color:#6c6b6b}.scielo__theme--dark .breadcrumb li.active{color:#adadad}.scielo__theme--light .breadcrumb li.active{color:#6c6b6b}.dropdown-divider{border-color:#ccc}.scielo__theme--dark .dropdown-divider{border-color:rgba(255,255,255,.3)}.scielo__theme--light .dropdown-divider{border-color:#ccc}.dropdown-menu{background:#fff;border-color:#ccc}.scielo__theme--dark .dropdown-menu{background:#333;border-color:rgba(255,255,255,.3)}.scielo__theme--light .dropdown-menu{background:#fff;border-color:#ccc}.dropdown-item{white-space:normal;color:#333}.dropdown-item:hover{color:#333;background:#f7f6f4}.scielo__theme--dark .dropdown-item{white-space:normal;color:#c4c4c4}.scielo__theme--dark .dropdown-item:hover{color:#c4c4c4;background:#414141}.scielo__theme--light .dropdown-item{white-space:normal;color:#333}.scielo__theme--light .dropdown-item:hover{color:#333;background:#f7f6f4}footer{border-top:0!important;margin:0;padding-top:.75rem;padding-bottom:3rem}footer section{border-top:1px dashed #ccc}.scielo__theme--dark footer section{border-color:rgba(255,255,255,.3)}.scielo__theme--light footer section{border-color:#ccc}footer section:first-child{border-top:0}footer .col{padding:15px 0}footer p{margin:0}footer .address-scielo{background-color:#f7f6f4}.scielo__theme--dark footer .address-scielo{background-color:#393939}.scielo__theme--light footer .address-scielo{background-color:#f7f6f4}footer .address-scielo .col{border:0}@media (max-width:575.98px){footer .address-scielo .col{padding-left:8px;padding-right:8px}footer .address-scielo .col:first-child{padding-bottom:0}footer .address-scielo .col:last-child{padding-top:0}}footer .partners{padding:16px 0}footer .partners a{margin:10px}@media (max-width:575.98px){footer .partners img{margin:8px 0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#333;text-align:left;background-color:transparent;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.scielo__theme--dark .accordion-button{color:#c4c4c4}.scielo__theme--light .accordion-button{color:#333}.accordion-button:not(.collapsed){color:#333;background-color:transparent;box-shadow:none;border-bottom:3px solid #3867ce}.scielo__theme--dark .accordion-button:not(.collapsed){color:#c4c4c4;background-color:transparent;border-bottom:3px solid #86acff}.scielo__theme--light .accordion-button:not(.collapsed){color:#333;background-color:transparent;border-bottom:3px solid #3867ce}.accordion-button:not(.collapsed)::after{background-image:none;transform:rotate(90deg);color:#3867ce}.scielo__theme--dark .accordion-button:not(.collapsed)::after{color:#86acff}.scielo__theme--light .accordion-button:not(.collapsed)::after{color:#3867ce}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;font-family:'Material Icons Outlined';content:"arrow_forward_ios";background-image:none;background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out;text-align:center;line-height:1.25rem}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#3867ce;outline:0;box-shadow:none}.accordion-header{margin-bottom:0}.accordion-item{margin-bottom:-1px;background-color:transparent;border:1px solid #ccc}.scielo__theme--dark .accordion-item{border:1px solid rgba(255,255,255,.3)}.scielo__theme--light .accordion-item{border:1px solid #ccc}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:last-of-type{margin-bottom:0;border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem;background:#f7f6f4;color:#333}.scielo__theme--dark .accordion-body{color:#c4c4c4;background:#414141}.scielo__theme--light .accordion-body{color:#333;background:#f7f6f4}.accordion-flush{border:1px solid #ccc;border-radius:.25rem}.scielo__theme--dark .accordion-flush{border:1px solid rgba(255,255,255,.3)}.scielo__theme--light .accordion-flush{border:1px solid #ccc}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.alert{padding:.75rem 3.75rem .75rem 1.5rem;border-radius:.1875rem;border:2px solid #86acff;background:#fff;color:#00314c}.scielo__theme--dark .alert{background:#333;color:#eee}.scielo__theme--light .alert{background:#fff;color:#00314c}.alert hr{border-top-color:#efeeec}.scielo__theme--dark .alert hr{border-top-color:#414141}.scielo__theme--light .alert hr{border-top-color:#efeeec}.alert-danger,.alert-info,.alert-success,.alert-warning{padding-left:3.75rem;position:relative}.alert-danger:before,.alert-info:before,.alert-success:before,.alert-warning:before{content:'';position:absolute;top:.75rem;left:1.5rem;font-family:'Material Icons Outlined';font-size:1.5rem;line-height:1.5rem}.alert-primary{color:#fff;background:#3867ce;border-color:#3867ce}.alert-primary .alert-heading,.alert-primary .alert-link{color:#fff!important}.alert-primary a{color:#fff;text-decoration:underline}.scielo__theme--dark .alert-primary{color:#333;background:#86acff;border-color:#86acff}.scielo__theme--dark .alert-primary .alert-heading,.scielo__theme--dark .alert-primary .alert-link{color:#333!important}.scielo__theme--light .alert-primary{color:#fff;background:#3867ce;border-color:#3867ce}.scielo__theme--light .alert-primary .alert-heading,.scielo__theme--light .alert-primary .alert-link{color:#fff!important}.alert-secondary{color:#fff;background:#fff;border-color:#fff;color:#333;background:#fff;border-color:rgba(0,0,0,.3)}.alert-secondary .alert-heading,.alert-secondary .alert-link{color:#fff!important}.alert-secondary a{color:#fff;text-decoration:underline}.scielo__theme--dark .alert-secondary{color:#333;background:#c4c4c4;border-color:#c4c4c4}.scielo__theme--dark .alert-secondary .alert-heading,.scielo__theme--dark .alert-secondary .alert-link{color:#333!important}.scielo__theme--light .alert-secondary{color:#fff;background:#fff;border-color:#fff}.scielo__theme--light .alert-secondary .alert-heading,.scielo__theme--light .alert-secondary .alert-link{color:#fff!important}.alert-secondary .alert-heading,.alert-secondary .alert-link{color:#333!important}.scielo__theme--dark .alert-secondary{color:#c4c4c4;background:0 0;border-color:#c4c4c4}.scielo__theme--dark .alert-secondary .alert-heading,.scielo__theme--dark .alert-secondary .alert-link{color:#c4c4c4!important}.scielo__theme--light .alert-secondary{color:#333;background:#fff;border-color:rgba(0,0,0,.3)}.scielo__theme--light .alert-secondary .alert-heading,.scielo__theme--light .alert-secondary .alert-link{color:#333!important}.alert-info{color:#fff;background:#2195a9;border-color:#2195a9}.alert-info:before{content:"info";color:inherit}.alert-info .alert-heading,.alert-info .alert-link{color:#fff!important}.alert-info a{color:#fff;text-decoration:underline}.scielo__theme--dark .alert-info{color:#333;background:#2299ad;border-color:#2299ad}.scielo__theme--dark .alert-info .alert-heading,.scielo__theme--dark .alert-info .alert-link{color:#333!important}.scielo__theme--light .alert-info{color:#fff;background:#2195a9;border-color:#2195a9}.scielo__theme--light .alert-info .alert-heading,.scielo__theme--light .alert-info .alert-link{color:#fff!important}.alert-dark{color:#fff;background:#3867ce;border-color:#3867ce}.alert-dark .alert-heading,.alert-dark .alert-link{color:#fff!important}.alert-dark a{color:#fff;text-decoration:underline}.scielo__theme--dark .alert-dark{color:#333;background:#86acff;border-color:#86acff}.scielo__theme--dark .alert-dark .alert-heading,.scielo__theme--dark .alert-dark .alert-link{color:#333!important}.scielo__theme--light .alert-dark{color:#fff;background:#3867ce;border-color:#3867ce}.scielo__theme--light .alert-dark .alert-heading,.scielo__theme--light .alert-dark .alert-link{color:#fff!important}.alert-success{color:#fff;background:#2c9d45;border-color:#2c9d45}.alert-success:before{content:"check_circle";color:inherit}.alert-success .alert-heading,.alert-success .alert-link{color:#fff!important}.alert-success a{color:#fff;text-decoration:underline}.scielo__theme--dark .alert-success{color:#333;background:#2c9d45;border-color:#2c9d45}.scielo__theme--dark .alert-success .alert-heading,.scielo__theme--dark .alert-success .alert-link{color:#333!important}.scielo__theme--light .alert-success{color:#fff;background:#2c9d45;border-color:#2c9d45}.scielo__theme--light .alert-success .alert-heading,.scielo__theme--light .alert-success .alert-link{color:#fff!important}.alert-danger{color:#fff;background:#c63800;border-color:#c63800}.alert-danger:before{content:"report_problem";color:inherit}.alert-danger .alert-heading,.alert-danger .alert-link{color:#fff!important}.alert-danger a{color:#fff;text-decoration:underline}.scielo__theme--dark .alert-danger{color:#333;background:#ff7e4a;border-color:#ff7e4a}.scielo__theme--dark .alert-danger .alert-heading,.scielo__theme--dark .alert-danger .alert-link{color:#333!important}.scielo__theme--light .alert-danger{color:#fff;background:#c63800;border-color:#c63800}.scielo__theme--light .alert-danger .alert-heading,.scielo__theme--light .alert-danger .alert-link{color:#fff!important}.alert-warning{color:#fff;background:#b67f00;border-color:#b67f00}.alert-warning:before{content:"report_problem";color:inherit}.alert-warning .alert-heading,.alert-warning .alert-link{color:#fff!important}.alert-warning a{color:#fff;text-decoration:underline}.scielo__theme--dark .alert-warning{color:#333;background:#b67f00;border-color:#b67f00}.scielo__theme--dark .alert-warning .alert-heading,.scielo__theme--dark .alert-warning .alert-link{color:#333!important}.scielo__theme--light .alert-warning{color:#fff;background:#b67f00;border-color:#b67f00}.scielo__theme--light .alert-warning .alert-heading,.scielo__theme--light .alert-warning .alert-link{color:#fff!important}.alert-link{color:#3867ce!important}.scielo__theme--dark .alert-link{color:#86acff!important}.scielo__theme--light .alert-link{color:#3867ce!important}.alert-dismissible{padding-right:3.75rem}.alert-dismissible .close{padding:.5625rem .75rem;font-size:1.5rem;line-height:1.5rem;color:inherit;top:0;right:0;opacity:.5;position:absolute;right:0;border:0;background:0 0}.alert-dismissible .close:before{font-family:'Material Icons Outlined';content:"close";color:inherit}.alert-dismissible .close:hover{opacity:1}.alert.text-center{border-radius:0}.alert.text-center:before{position:static;display:block}@media (min-width:992px){.alert.text-center:before{display:inline-block;vertical-align:bottom}}.card{position:relative;display:flex;flex-direction:column;min-width:0;height:auto;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid #ccc;border-radius:.25rem}.card:has(.card-footer){padding-bottom:4rem}.scielo__theme--dark .card{border:1px solid rgba(255,255,255,.3);background-color:#333}.scielo__theme--light .card{border:1px solid #ccc;background-color:#fff}.journalContent .card{min-height:220px}.card .list-group-item{background-color:#fff;border-color:#ccc}.scielo__theme--dark .card .list-group-item{border-color:rgba(255,255,255,.3);background-color:#333}.scielo__theme--light .card .list-group-item{border-color:#ccc;background-color:#fff}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem;color:#333}.scielo__theme--dark .card-body{color:#c4c4c4}.scielo__theme--light .card-body{color:#333}.card-body .btn{position:absolute;bottom:16px}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0;font-weight:700;text-transform:uppercase;color:#6c6b6b;font-size:.75rem}.scielo__theme--dark .card-subtitle{color:#adadad}.scielo__theme--light .card-subtitle{color:#6c6b6b}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:1rem;position:absolute;left:0;bottom:0;right:0;background-color:transparent;border:0}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%;height:9.3rem;object-fit:cover}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.5rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.card .stretched-link:hover .text-primary{text-decoration:underline;color:#2d52a5!important}.card .stretched-link:hover .btn-secondary{background:#d9d9d9 radial-gradient(circle,transparent 1%,#d9d9d9 1%) center/15000%}.card .stretched-link:hover .card-img-top{transition:all .8s;filter:brightness(.83)}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1060;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.5rem;outline:0}.scielo__theme--dark .modal-content{border:1px solid rgba(255,255,255,.3);background-color:#333}.scielo__theme--light .modal-content{border:1px solid #ccc;background-color:#fff}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:flex-start;justify-content:space-between;padding:1rem 1rem;border-top-left-radius:calc(.5rem - 1px);border-top-right-radius:calc(.5rem - 1px);border-bottom:1px solid #ccc}.scielo__theme--dark .modal-header{border-bottom:1px solid rgba(255,255,255,.3)}.scielo__theme--light .modal-header{border-bottom:1px solid #ccc}.modal-header .btn-close{padding:.5rem .5rem;margin:-2px -.5rem -.5rem auto;background-image:none;font-weight:700;font-size:1.3125rem;line-height:1.25em;letter-spacing:0;text-align:center;position:relative}.modal-header .btn-close:before{font-family:'Material Icons Outlined';content:"close";color:#333;line-height:1.8;position:absolute;top:0;left:0;text-align:center;width:100%}.scielo__theme--dark .modal-header .btn-close:before{color:#c4c4c4}.scielo__theme--light .modal-header .btn-close:before{color:#333}.modal-title{margin-bottom:0;line-height:1.5}.modal-title [class*=" material-icons"],.modal-title [class^=material-icons]{vertical-align:bottom}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-bottom-right-radius:calc(.5rem - 1px);border-bottom-left-radius:calc(.5rem - 1px);border-top:1px solid #ccc}.scielo__theme--dark .modal-footer{border-top:1px solid rgba(255,255,255,.3)}.scielo__theme--light .modal-footer{border-top:1px solid #ccc}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.table{--scielo-table-bg:transparent;--scielo-table-striped-color:#393939;--scielo-table-striped-bg:rgba(0, 0, 0, 0.05);--scielo-table-active-color:#393939;--scielo-table-active-bg:rgba(0, 0, 0, 0.1);--scielo-table-hover-color:#393939;--scielo-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#333;vertical-align:top;border-color:#ccc}.scielo__theme--dark .table{color:#c4c4c4;border-color:rgba(255,255,255,.3)}.scielo__theme--light .table{color:#333;border-color:#ccc}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--scielo-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--scielo-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>thead th{border-bottom:2px solid #ccc}.scielo__theme--dark .table>thead th{border-bottom:2px solid rgba(255,255,255,.3)}.scielo__theme--light .table>thead th{border-bottom:2px solid #ccc}.table>:not(:last-child)>:last-child>*{border-bottom-color:#ccc}.scielo__theme--dark .table>:not(:last-child)>:last-child>*{border-bottom-color:rgba(255,255,255,.3)}.scielo__theme--light .table>:not(:last-child)>:last-child>*{border-bottom-color:#ccc}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-striped>tbody>tr:nth-of-type(odd){--scielo-table-accent-bg:var(--scielo-table-striped-bg);color:#333}.scielo__theme--dark .table-striped>tbody>tr:nth-of-type(odd){color:#c4c4c4}.scielo__theme--light .table-striped>tbody>tr:nth-of-type(odd){color:#333}.table-active{--scielo-table-accent-bg:var(--scielo-table-active-bg);color:#333}.scielo__theme--dark .table-active{color:#c4c4c4}.scielo__theme--light .table-active{color:#333}.table-hover>tbody>tr:hover{--scielo-table-accent-bg:var(--scielo-table-hover-bg);color:#333}.scielo__theme--dark .table-hover>tbody>tr:hover{color:#c4c4c4}.scielo__theme--light .table-hover>tbody>tr:hover{color:#333}.table-primary{--scielo-table-bg:rgba(56, 103, 206, 0.7);--scielo-table-striped-bg:rgba(51, 94, 188, 0.715);--scielo-table-striped-color:#fff;--scielo-table-active-bg:rgba(46, 85, 171, 0.73);--scielo-table-active-color:#fff;--scielo-table-hover-bg:rgba(49, 90, 179, 0.7225);--scielo-table-hover-color:#fff;color:#000}.table-secondary{--scielo-table-bg:rgba(255, 255, 255, 0.5);--scielo-table-striped-bg:rgba(220, 220, 220, 0.525);--scielo-table-striped-color:#000;--scielo-table-active-bg:rgba(191, 191, 191, 0.55);--scielo-table-active-color:#000;--scielo-table-hover-bg:rgba(205, 205, 205, 0.5375);--scielo-table-hover-color:#000;color:#000}.table-success{--scielo-table-bg:rgba(44, 157, 69, 0.7);--scielo-table-striped-bg:rgba(40, 143, 63, 0.715);--scielo-table-striped-color:#000;--scielo-table-active-bg:rgba(36, 130, 57, 0.73);--scielo-table-active-color:#fff;--scielo-table-hover-bg:rgba(38, 136, 60, 0.7225);--scielo-table-hover-color:#000;color:#000}.table-info{--scielo-table-bg:rgba(33, 149, 169, 0.7);--scielo-table-striped-bg:rgba(30, 136, 154, 0.715);--scielo-table-striped-color:#000;--scielo-table-active-bg:rgba(27, 124, 140, 0.73);--scielo-table-active-color:#fff;--scielo-table-hover-bg:rgba(29, 130, 147, 0.7225);--scielo-table-hover-color:#fff;color:#000}.table-warning{--scielo-table-bg:rgba(182, 127, 0, 0.7);--scielo-table-striped-bg:rgba(166, 116, 0, 0.715);--scielo-table-striped-color:#000;--scielo-table-active-bg:rgba(151, 105, 0, 0.73);--scielo-table-active-color:#fff;--scielo-table-hover-bg:rgba(158, 110, 0, 0.7225);--scielo-table-hover-color:#000;color:#000}.table-danger{--scielo-table-bg:rgba(198, 56, 0, 0.7);--scielo-table-striped-bg:rgba(180, 51, 0, 0.715);--scielo-table-striped-color:#fff;--scielo-table-active-bg:rgba(164, 46, 0, 0.73);--scielo-table-active-color:#fff;--scielo-table-hover-bg:rgba(172, 49, 0, 0.7225);--scielo-table-hover-color:#fff;color:#000}.table-light{--scielo-table-bg:#F7F6F4;--scielo-table-striped-bg:#ebeae8;--scielo-table-striped-color:#000;--scielo-table-active-bg:#dedddc;--scielo-table-active-color:#000;--scielo-table-hover-bg:#e4e4e2;--scielo-table-hover-color:#000;color:#000}.table-dark{--scielo-table-bg:#393939;--scielo-table-striped-bg:#434343;--scielo-table-striped-color:#fff;--scielo-table-active-bg:#4d4d4d;--scielo-table-active-color:#fff;--scielo-table-hover-bg:#484848;--scielo-table-hover-color:#fff;color:#fff}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.table table{min-width:100%}.nav:not(.flex-column).nav-tabs{border:none;background:0 0;margin-bottom:.75rem;border-bottom:1px solid #ccc}.scielo__theme--dark .nav:not(.flex-column).nav-tabs{border-color:rgba(255,255,255,.3)}.scielo__theme--light .nav:not(.flex-column).nav-tabs{border-color:#ccc}@media screen and (max-width:575px){.nav:not(.flex-column).nav-tabs{display:block;white-space:nowrap;overflow-x:auto;overflow-y:hidden}.nav:not(.flex-column).nav-tabs>li{float:none;display:inline-block}}.nav:not(.flex-column).nav-tabs .nav-link{font-weight:400!important;font-size:1rem;letter-spacing:.1px!important;white-space:nowrap;line-height:1.875rem;color:#333;border-radius:0;border-top-left-radius:.1875rem;border-top-right-radius:.1875rem;border-width:0;padding:.5625rem 1.5rem;position:relative;transition:.2s all}.nav:not(.flex-column).nav-tabs .nav-link:before{content:'';position:absolute;bottom:0;left:50%;transform:translateX(-50%);width:0;border-bottom:3px solid rgba(56,103,206,.25);transition:.2s all}.scielo__theme--dark .nav:not(.flex-column).nav-tabs .nav-link{color:#c4c4c4}.scielo__theme--light .nav:not(.flex-column).nav-tabs .nav-link{color:#333}.nav:not(.flex-column).nav-tabs .nav-link.active{border:none;background:0 0;color:#00314c}.scielo__theme--dark .nav:not(.flex-column).nav-tabs .nav-link.active{color:#eee}.scielo__theme--light .nav:not(.flex-column).nav-tabs .nav-link.active{color:#00314c}.nav:not(.flex-column).nav-tabs .nav-link.active:before{left:50%;width:100%;border-color:#3867ce}.scielo__theme--dark .nav:not(.flex-column).nav-tabs .nav-link.active:before{border-color:#86acff}.scielo__theme--light .nav:not(.flex-column).nav-tabs .nav-link.active:before{border-color:#3867ce}.nav:not(.flex-column).nav-tabs .nav-link.disabled{color:rgba(0,0,0,.396);cursor:not-allowed}.scielo__theme--dark .nav:not(.flex-column).nav-tabs .nav-link.disabled{color:rgba(255,255,255,.2)}.scielo__theme--light .nav:not(.flex-column).nav-tabs .nav-link.disabled{color:rgba(0,0,0,.396)}.nav:not(.flex-column).nav-tabs .nav-link.dropdown-toggle{position:relative;padding-right:2.25rem!important}.nav:not(.flex-column).nav-tabs .nav-link.dropdown-toggle:after{position:absolute;top:50%;transform:translateY(-50%);font-family:'Material Icons Outlined';content:"arrow_drop_down";color:inherit;border:0;line-height:1.5rem!important;text-align:center;right:1rem;width:1.25rem;height:1.25rem;font-size:1.25rem;line-height:1.25rem;border:none!important;margin-top:0!important;width:1.5rem!important;height:1.5rem!important;transform:translateY(-50%)}.nav:not(.flex-column).nav-tabs .nav-link:not(.disabled):focus,.nav:not(.flex-column).nav-tabs .nav-link:not(.disabled):hover{background:#f7f6f4;color:#00314c}.scielo__theme--dark .nav:not(.flex-column).nav-tabs .nav-link:not(.disabled):focus,.scielo__theme--dark .nav:not(.flex-column).nav-tabs .nav-link:not(.disabled):hover{background:#414141;color:#eee}.scielo__theme--light .nav:not(.flex-column).nav-tabs .nav-link:not(.disabled):focus,.scielo__theme--light .nav:not(.flex-column).nav-tabs .nav-link:not(.disabled):hover{background:#f7f6f4;color:#00314c}.nav:not(.flex-column).nav-tabs .dropdown-menu{background:#fff;border:1px solid #ccc;border-radius:3px}.nav:not(.flex-column).nav-tabs .dropdown-menu>a{position:relative;font-weight:700;font-size:1;line-height:1.2em;letter-spacing:0;padding:.75rem 1.5rem .75rem;line-height:1.5rem;transition:all .5s;border-radius:.1875rem;background:0 0;color:#fff;font-weight:400;font-size:1rem;line-height:1.2em;color:#333!important}.scielo__theme--dark .nav:not(.flex-column).nav-tabs .dropdown-menu>a{color:#eee}.scielo__theme--light .nav:not(.flex-column).nav-tabs .dropdown-menu>a{color:#fff}.nav:not(.flex-column).nav-tabs .dropdown-menu>a:hover:hover{background:#f7f6f4;color:#333}.scielo__theme--dark .nav:not(.flex-column).nav-tabs .dropdown-menu>a:hover:hover{background:#414141;color:#c4c4c4}.scielo__theme--light .nav:not(.flex-column).nav-tabs .dropdown-menu>a:hover:hover{background:#f7f6f4;color:#333}.scielo__theme--dark .nav:not(.flex-column).nav-tabs .dropdown-menu>a{color:#c4c4c4!important}.scielo__theme--light .nav:not(.flex-column).nav-tabs .dropdown-menu>a{color:#333!important}.scielo__theme--dark .nav:not(.flex-column).nav-tabs .dropdown-menu{background:#333;border:1px solid rgba(255,255,255,.3)}.scielo__theme--light .nav:not(.flex-column).nav-tabs .dropdown-menu{background:#fff;border:1px solid #ccc}.nav:not(.flex-column).nav-tabs .dropdown-menu .dropdown-divider{border-color:#ccc}.scielo__theme--dark .nav:not(.flex-column).nav-tabs .dropdown-menu .dropdown-divider{border-color:rgba(255,255,255,.3)}.scielo__theme--light .nav:not(.flex-column).nav-tabs .dropdown-menu .dropdown-divider{border-color:#ccc}.nav.nav-pills .nav-link{font-weight:400!important;font-size:1rem;letter-spacing:.1px!important;white-space:nowrap;line-height:1.875rem;color:#333;border-radius:.1875rem;border-width:0;padding:.5625rem 1.5rem;position:relative;transition:.2s all}.scielo__theme--dark .nav.nav-pills .nav-link{color:#c4c4c4}.scielo__theme--light .nav.nav-pills .nav-link{color:#333}.nav.nav-pills .nav-link.active{border:none;background:#3867ce;color:#fff}.scielo__theme--dark .nav.nav-pills .nav-link.active{background:#86acff;color:#333}.scielo__theme--light .nav.nav-pills .nav-link.active{background:#3867ce;color:#fff}.nav.nav-pills .nav-link.disabled{color:rgba(0,0,0,.396);cursor:not-allowed}.scielo__theme--dark .nav.nav-pills .nav-link.disabled{color:rgba(255,255,255,.2)}.scielo__theme--light .nav.nav-pills .nav-link.disabled{color:rgba(0,0,0,.396)}.nav.nav-pills .nav-link:not(.disabled):focus:not(.active),.nav.nav-pills .nav-link:not(.disabled):hover:not(.active){background:#f7f6f4;color:#00314c}.scielo__theme--dark .nav.nav-pills .nav-link:not(.disabled):focus:not(.active),.scielo__theme--dark .nav.nav-pills .nav-link:not(.disabled):hover:not(.active){background:#414141;color:#eee}.scielo__theme--light .nav.nav-pills .nav-link:not(.disabled):focus:not(.active),.scielo__theme--light .nav.nav-pills .nav-link:not(.disabled):hover:not(.active){background:#f7f6f4;color:#00314c}.nav.flex-column.nav-pills .nav-link{font-weight:400!important;font-size:1rem;letter-spacing:.1px!important;white-space:nowrap;background:0 0;line-height:1.875rem;color:#333;border-radius:.1875rem;border-left:3px solid transparent;border-top-left-radius:0;border-bottom-left-radius:0;padding:.5625rem 1.5rem}.scielo__theme--dark .nav.flex-column.nav-pills .nav-link{color:#c4c4c4}.scielo__theme--light .nav.flex-column.nav-pills .nav-link{color:#333}.nav.flex-column.nav-pills .nav-link.active{background:0 0;border-color:#3867ce;color:#00314c}.scielo__theme--dark .nav.flex-column.nav-pills .nav-link.active{border-color:#86acff;color:#eee}.scielo__theme--light .nav.flex-column.nav-pills .nav-link.active{border-color:#3867ce;color:#00314c}.nav.flex-column.nav-pills .nav-link.disabled{color:rgba(0,0,0,.396);cursor:not-allowed}.scielo__theme--dark .nav.flex-column.nav-pills .nav-link.disabled{color:rgba(255,255,255,.2)}.scielo__theme--light .nav.flex-column.nav-pills .nav-link.disabled{color:rgba(0,0,0,.396)}.nav.flex-column.nav-pills .nav-link:not(.disabled):focus,.nav.flex-column.nav-pills .nav-link:not(.disabled):hover{background:#f7f6f4;color:#00314c}.scielo__theme--dark .nav.flex-column.nav-pills .nav-link:not(.disabled):focus,.scielo__theme--dark .nav.flex-column.nav-pills .nav-link:not(.disabled):hover{background:#414141;color:#eee}.scielo__theme--light .nav.flex-column.nav-pills .nav-link:not(.disabled):focus,.scielo__theme--light .nav.flex-column.nav-pills .nav-link:not(.disabled):hover{background:#f7f6f4;color:#00314c}.tab-pane{padding:.75rem 0}.slick-slider{position:relative;display:block;box-sizing:border-box;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-ms-touch-action:pan-y;touch-action:pan-y;-webkit-tap-highlight-color:transparent}.slick-list{position:relative;overflow:hidden;display:block;margin:0;padding:0}.slick-list:focus{outline:0}.slick-list.dragging{cursor:pointer;cursor:hand}.slick-slider .slick-list,.slick-slider .slick-track{-webkit-transform:translate3d(0,0,0);-moz-transform:translate3d(0,0,0);-ms-transform:translate3d(0,0,0);-o-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.slick-track{position:relative;left:0;top:0;display:flex}.slick-track:after,.slick-track:before{content:"";display:table}.slick-track:after{clear:both}.slick-loading .slick-track{visibility:hidden}.slick-slide{float:left;min-height:1px;margin:0 10px;display:none}[dir=rtl] .slick-slide{float:right}.slick-slide img{display:block}.slick-slide.slick-loading img{display:none}.slick-slide.dragging img{pointer-events:none}.slick-initialized .slick-slide{display:block}.slick-loading .slick-slide{visibility:hidden}.slick-vertical .slick-slide{display:block;height:auto;border:1px solid transparent}.slick-arrow.slick-hidden{display:none}.slick-next,.slick-prev{position:relative;display:inline-block;padding:.625rem 1rem;border-radius:.25rem;line-height:1.25rem;height:2.5rem;font-weight:400!important;font-size:1rem;letter-spacing:.1px!important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;background-position:center;transition:all .8s;margin:0 0 1rem;background-color:#fff;border:1px solid #ccc;color:#333;position:absolute;display:block;height:40px;width:30px;line-height:0;font-size:0;cursor:pointer;top:50%;-webkit-transform:translate(0,-50%);-ms-transform:translate(0,-50%);transform:translate(0,-50%);padding:0}.slick-next:focus,.slick-prev:focus{box-shadow:0 0 0 .125rem rgba(56,103,206,.25);outline:0}.slick-next:focus:active,.slick-prev:focus:active{box-shadow:0 0 0 .25rem rgba(56,103,206,.25)}.slick-next:focus,.slick-prev:focus{box-shadow:0 0 0 .125rem rgba(204,204,204,.25);outline:0}.slick-next:focus-visible,.slick-prev:focus-visible{outline:3px solid #3867ce;outline-offset:2px}.slick-next:active,.slick-prev:active{box-shadow:0 0 0 .25rem rgba(204,204,204,.25)}.slick-next:focus,.slick-prev:focus{background-color:#fff;color:#333}.slick-next:focus-visible,.slick-prev:focus-visible{outline:3px solid #3867ce;outline-offset:2px}.slick-next.active:not(:disabled):not(.disabled),.slick-prev.active:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background-color:#d9d9d9;color:#333}.show>.slick-next.dropdown-toggle,.show>.slick-prev.dropdown-toggle{background-color:#d9d9d9;color:#333}.slick-next:hover:not(:disabled):not(.disabled),.slick-prev:hover:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background:#d9d9d9 radial-gradient(circle,transparent 1%,#d9d9d9 1%) center/15000%;color:#333;text-decoration:none}.slick-next:active:not(:disabled):not(.disabled),.slick-prev:active:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background-color:#1a1a1a;background-size:100%;transition:background 0s;color:#333}.slick-next:hover:not(:disabled):not(.disabled),.slick-prev:hover:not(:disabled):not(.disabled){border:1px solid #ccc;background:hoverBgColor radial-gradient(circle,transparent 1%,hoverBgColor 1%) center/15000%;color:#333}.slick-next:active:not(:disabled):not(.disabled),.slick-prev:active:not(:disabled):not(.disabled){border:1px solid #ccc;background-color:gray;background-size:100%;transition:background 0s;color:#333}.slick-next:focus,.slick-prev:focus{border-color:#ccc}.scielo__theme--dark .slick-next,.scielo__theme--dark .slick-prev{background-color:#c4c4c4;border:1px solid rgba(255,255,255,.3);color:#333}.scielo__theme--dark .slick-next:focus,.scielo__theme--dark .slick-prev:focus{background-color:#c4c4c4;color:#333}.scielo__theme--dark .slick-next.active:not(:disabled):not(.disabled),.scielo__theme--dark .slick-prev.active:not(:disabled):not(.disabled){background-color:#a7a7a7;color:#333}.scielo__theme--dark .slick-next:hover:not(:disabled):not(.disabled),.scielo__theme--dark .slick-prev:hover:not(:disabled):not(.disabled){border:1px solid #dcdcdc;background:#dcdcdc radial-gradient(circle,transparent 1%,#dcdcdc 1%) center/15000%;color:#333;text-decoration:none}.scielo__theme--dark .slick-next:active:not(:disabled):not(.disabled),.scielo__theme--dark .slick-prev:active:not(:disabled):not(.disabled){border:1px solid #dcdcdc;background-color:#f9f9f9;background-size:100%;transition:background 0s;color:#333}.scielo__theme--dark .slick-next:hover:not(:disabled):not(.disabled),.scielo__theme--dark .slick-prev:hover:not(:disabled):not(.disabled){border:1px solid rgba(255,255,255,.3);background:hoverBgColor radial-gradient(circle,transparent 1%,hoverBgColor 1%) center/15000%;color:#333}.scielo__theme--dark .slick-next:active:not(:disabled):not(.disabled),.scielo__theme--dark .slick-prev:active:not(:disabled):not(.disabled){border:1px solid rgba(255,255,255,.3);background-color:#f9f9f9;background-size:100%;transition:background 0s;color:#333}.scielo__theme--dark .slick-next:focus,.scielo__theme--dark .slick-prev:focus{border-color:rgba(255,255,255,.3)}.scielo__theme--light .slick-next,.scielo__theme--light .slick-prev{background-color:#fff;border-color:1px solid #ccc;color:#333}.scielo__theme--light .slick-next:focus,.scielo__theme--light .slick-prev:focus{background-color:#fff;color:#333}.scielo__theme--light .slick-next.active:not(:disabled):not(.disabled),.scielo__theme--light .slick-prev.active:not(:disabled):not(.disabled){background-color:#fff;color:#333}.scielo__theme--light .slick-next:hover:not(:disabled):not(.disabled),.scielo__theme--light .slick-prev:hover:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background:#d9d9d9 radial-gradient(circle,transparent 1%,#d9d9d9 1%) center/15000%;color:#333;text-decoration:none}.scielo__theme--light .slick-next:active:not(:disabled):not(.disabled),.scielo__theme--light .slick-prev:active:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background-color:#1a1a1a;background-size:100%;transition:background 0s;color:#333}.scielo__theme--light .slick-next:hover:not(:disabled):not(.disabled),.scielo__theme--light .slick-prev:hover:not(:disabled):not(.disabled){border:1px solid #ccc;background:hoverBgColor radial-gradient(circle,transparent 1%,hoverBgColor 1%) center/15000%;color:#333}.scielo__theme--light .slick-next:active:not(:disabled):not(.disabled),.scielo__theme--light .slick-prev:active:not(:disabled):not(.disabled){border:1px solid #ccc;background-color:gray;background-size:100%;transition:background 0s;color:#333}.scielo__theme--light .slick-next:focus,.scielo__theme--light .slick-prev:focus{border-color:#ccc}.slick-next:focus,.slick-next:hover,.slick-prev:focus,.slick-prev:hover{outline:0}.slick-next:focus:before,.slick-next:hover:before,.slick-prev:focus:before,.slick-prev:hover:before{opacity:1}.slick-next.slick-disabled:before,.slick-prev.slick-disabled:before{opacity:.25}.slick-next:before,.slick-prev:before{font-family:"Material Icons Outlined";font-size:20px;line-height:1;opacity:.75;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.slick-prev{left:-25px}@media (max-width:575.98px){.slick-prev{left:0;z-index:1}}[dir=rtl] .slick-prev{left:auto;right:-25px}.slick-prev:before{content:"navigate_before"}[dir=rtl] .slick-prev:before{content:"navigate_next"}.slick-next{right:-25px}@media (max-width:575.98px){.slick-next{right:0;z-index:1}}[dir=rtl] .slick-next{left:-25px;right:auto}.slick-next:before{content:"navigate_next"}[dir=rtl] .slick-next:before{content:"navigate_before"}.slick-dotted.slick-slider{margin-bottom:30px}.slick-dots{position:absolute;bottom:-32px;list-style:none;display:block;text-align:center;padding:0;margin:0;width:100%}@media (max-width:575.98px){.slick-dots{display:flex}}.slick-dots li{position:relative;display:inline-block;height:16px;width:16px;margin:0 5px;padding:0;cursor:pointer}@media (max-width:575.98px){.slick-dots li{flex:1;height:4px}}.slick-dots li button{background:0 0;border:2px solid #6c6b6b;border-radius:100px;display:block;height:16px;width:16px;outline:0;line-height:0;font-size:0;color:transparent;padding:0;cursor:pointer}@media (max-width:575.98px){.slick-dots li button{background:rgba(108,107,107,.2);border:0;width:100%;height:4px;border-radius:0}}.scielo__theme--dark .slick-dots li button{background:rgba(173,173,173,.2)}.scielo__theme--light .slick-dots li button{background:rgba(108,107,107,.2)}.slick-dots li button:focus,.slick-dots li button:hover{outline:0}.slick-dots li button:focus:before,.slick-dots li button:hover:before{opacity:1}.slick-dots li.slick-active button{background:#6c6b6b}.scielo__theme--dark .slick-dots li.slick-active button{background:#adadad}.scielo__theme--light .slick-dots li.slick-active button{background:#6c6b6b}.scielo__language{text-align:right}@media (min-width:576px){.scielo__language{position:static;display:inline-block;float:right}}.scielo__levelMenu{background:#f7f6f4;padding:1.125rem}.scielo__theme--dark .scielo__levelMenu{background:#393939}.scielo__theme--light .scielo__levelMenu{background:#f7f6f4}.scielo__levelMenu>.container{margin:0!important}.scielo__levelMenu>.container [class^=col]{text-align:center;border-right:1px dashed #ccc;line-height:3.75rem}.scielo__theme--dark .scielo__levelMenu>.container [class^=col]{border-color:rgba(255,255,255,.3)}.scielo__theme--light .scielo__levelMenu>.container [class^=col]{border-color:#ccc}.scielo__levelMenu>.container [class^=col]:last-of-type{border:0}@media (max-width:575.98px){.scielo__levelMenu>.container [class^=col]{border:0}.scielo__levelMenu>.container [class^=col]:first-of-type{border-right:1px dashed #ccc}.scielo__theme--dark .scielo__levelMenu>.container [class^=col]:first-of-type{border-color:rgba(255,255,255,.3)}.scielo__theme--light .scielo__levelMenu>.container [class^=col]:first-of-type{border-color:#ccc}.scielo__levelMenu>.container [class^=col]:last-of-type{margin-top:1rem}}.scielo__levelMenu a{text-decoration:none;font-weight:700}.journal .levelMenu{margin-top:1.5rem}@media (max-width:575.98px){.journal .levelMenu{margin-top:145px}}section.scielo__search-articles{background:#f7f6f4;padding:1.125rem}.scielo__theme--dark section.scielo__search-articles{background:#393939}.scielo__theme--light section.scielo__search-articles{background:#f7f6f4}@media (max-width:991.98px){section.scielo__search-articles .input-group{flex-flow:column;height:auto;background:0 0}section.scielo__search-articles .input-group .form-control,section.scielo__search-articles .input-group .form-select{width:100%;margin:0 0 .5rem!important;border-radius:.25rem!important}section.scielo__search-articles .input-group .form-control:last-child,section.scielo__search-articles .input-group .form-select:last-child{margin:0}}section.scielo__search-articles .input-group .input-group-append .form-select{min-width:180px}section.scielo__search-articles .input-group .input-group-preppend .form-select{min-width:120px}.scielo__contribGroup{color:#403d39;margin:15px 10%;font-size:1.1em;text-align:center;opacity:1}.scielo__contribGroup a.btn-fechar{display:inline-block;border-radius:100%;cursor:pointer;width:30px;height:30px;font-size:86%;padding:5px 0;text-align:center;margin-top:10px;font-family:'Material Icons Outlined';content:"close"}.scielo__contribGroup a.btn-fechar:hover{background:#3867ce;color:#fff}.scielo__contribGroup .sci-ico-emailOutlined{font-size:20px;vertical-align:baseline}.scielo__contribGroup .dropdown{display:inline-block;padding:0}.scielo__contribGroup .dropdown .dropdown-toggle{background:0 0;border:1px solid transparent;padding:.625rem;height:auto;outline:0;margin-bottom:0;color:#3867ce}.scielo__contribGroup .dropdown .dropdown-toggle:hover{border:1px solid transparent;color:#254895;background:0 0!important}.scielo__theme--dark .scielo__contribGroup .dropdown .dropdown-toggle{border:1px solid transparent;color:#86acff}.scielo__theme--dark .scielo__contribGroup .dropdown .dropdown-toggle:hover{border:1px solid transparent;color:#d3e0ff}.scielo__theme--light .scielo__contribGroup .dropdown .dropdown-toggle{border:1px solid transparent;color:#3867ce}.scielo__theme--light .scielo__contribGroup .dropdown .dropdown-toggle:hover{border:1px solid transparent;color:#254895}.scielo__contribGroup .dropdown .dropdown-toggle:focus{box-shadow:none;background:0 0;border:1px solid #ccc}.scielo__theme--dark .scielo__contribGroup .dropdown .dropdown-toggle:focus{box-shadow:none;background:0 0;border:1px solid rgba(255,255,255,.3)}.scielo__theme--light .scielo__contribGroup .dropdown .dropdown-toggle:focus{box-shadow:none;background:0 0;border:1px solid #ccc}@media (max-width:575.98px){.scielo__contribGroup .dropdown .dropdown-toggle{max-width:300px!important;white-space:inherit;height:auto}}.scielo__contribGroup .dropdown .dropdown-toggle:after{display:none}.scielo__contribGroup .dropdown .dropdown-menu{padding:10px 20px;text-align:left;border-radius:4px}.scielo__contribGroup .dropdown .dropdown-menu.show{color:#333;padding-left:.75rem}.scielo__theme--dark .scielo__contribGroup .dropdown .dropdown-menu.show{color:#c4c4c4}.scielo__theme--light .scielo__contribGroup .dropdown .dropdown-menu.show{color:#333}.scielo__contribGroup .dropdown .dropdown-menu strong{display:inline-block;margin:20px 0 8px 0;font-size:11px;color:#00314c;text-transform:uppercase}.scielo__contribGroup .dropdown a{cursor:pointer}.scielo__contribGroup .dropdown a span{display:inline-block;padding:5px 0}.scielo__contribGroup .dropdown.open{background:#3867ce;border-radius:4px}.scielo__contribGroup .dropdown.open a{color:#fff}.scielo__contribGroup .outlineFadeLink{background-color:#fff;border:1px solid #ccc;color:#333;margin:0 0 0 .625rem}.scielo__contribGroup .outlineFadeLink:focus{box-shadow:0 0 0 .125rem rgba(204,204,204,.25);outline:0}.scielo__contribGroup .outlineFadeLink:focus-visible{outline:3px solid #3867ce;outline-offset:2px}.scielo__contribGroup .outlineFadeLink:active{box-shadow:0 0 0 .25rem rgba(204,204,204,.25)}.scielo__contribGroup .outlineFadeLink:focus{background-color:#fff;color:#333}.scielo__contribGroup .outlineFadeLink:focus-visible{outline:3px solid #3867ce;outline-offset:2px}.scielo__contribGroup .outlineFadeLink.active:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background-color:#d9d9d9;color:#333}.show>.scielo__contribGroup .outlineFadeLink.dropdown-toggle{background-color:#d9d9d9;color:#333}.scielo__contribGroup .outlineFadeLink:hover:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background:#d9d9d9 radial-gradient(circle,transparent 1%,#d9d9d9 1%) center/15000%;color:#333;text-decoration:none}.scielo__contribGroup .outlineFadeLink:active:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background-color:#1a1a1a;background-size:100%;transition:background 0s;color:#333}.scielo__contribGroup .outlineFadeLink:hover:not(:disabled):not(.disabled){border:1px solid #ccc;background:hoverBgColor radial-gradient(circle,transparent 1%,hoverBgColor 1%) center/15000%;color:#333}.scielo__contribGroup .outlineFadeLink:active:not(:disabled):not(.disabled){border:1px solid #ccc;background-color:gray;background-size:100%;transition:background 0s;color:#333}.scielo__contribGroup .outlineFadeLink:focus{border-color:#ccc}.scielo__theme--dark .scielo__contribGroup .outlineFadeLink{background-color:#c4c4c4;border:1px solid rgba(255,255,255,.3);color:#333}.scielo__theme--dark .scielo__contribGroup .outlineFadeLink:focus{background-color:#c4c4c4;color:#333}.scielo__theme--dark .scielo__contribGroup .outlineFadeLink.active:not(:disabled):not(.disabled){background-color:#a7a7a7;color:#333}.scielo__theme--dark .scielo__contribGroup .outlineFadeLink:hover:not(:disabled):not(.disabled){border:1px solid #dcdcdc;background:#dcdcdc radial-gradient(circle,transparent 1%,#dcdcdc 1%) center/15000%;color:#333;text-decoration:none}.scielo__theme--dark .scielo__contribGroup .outlineFadeLink:active:not(:disabled):not(.disabled){border:1px solid #dcdcdc;background-color:#f9f9f9;background-size:100%;transition:background 0s;color:#333}.scielo__theme--dark .scielo__contribGroup .outlineFadeLink:hover:not(:disabled):not(.disabled){border:1px solid rgba(255,255,255,.3);background:hoverBgColor radial-gradient(circle,transparent 1%,hoverBgColor 1%) center/15000%;color:#333}.scielo__theme--dark .scielo__contribGroup .outlineFadeLink:active:not(:disabled):not(.disabled){border:1px solid rgba(255,255,255,.3);background-color:#f9f9f9;background-size:100%;transition:background 0s;color:#333}.scielo__theme--dark .scielo__contribGroup .outlineFadeLink:focus{border-color:rgba(255,255,255,.3)}.scielo__theme--light .scielo__contribGroup .outlineFadeLink{background-color:#fff;border-color:1px solid #ccc;color:#333}.scielo__theme--light .scielo__contribGroup .outlineFadeLink:focus{background-color:#fff;color:#333}.scielo__theme--light .scielo__contribGroup .outlineFadeLink.active:not(:disabled):not(.disabled){background-color:#fff;color:#333}.scielo__theme--light .scielo__contribGroup .outlineFadeLink:hover:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background:#d9d9d9 radial-gradient(circle,transparent 1%,#d9d9d9 1%) center/15000%;color:#333;text-decoration:none}.scielo__theme--light .scielo__contribGroup .outlineFadeLink:active:not(:disabled):not(.disabled){border:1px solid #d9d9d9;background-color:#1a1a1a;background-size:100%;transition:background 0s;color:#333}.scielo__theme--light .scielo__contribGroup .outlineFadeLink:hover:not(:disabled):not(.disabled){border:1px solid #ccc;background:hoverBgColor radial-gradient(circle,transparent 1%,hoverBgColor 1%) center/15000%;color:#333}.scielo__theme--light .scielo__contribGroup .outlineFadeLink:active:not(:disabled):not(.disabled){border:1px solid #ccc;background-color:gray;background-size:100%;transition:background 0s;color:#333}.scielo__theme--light .scielo__contribGroup .outlineFadeLink:focus{border-color:#ccc}@media (max-width:575.98px){.scielo__contribGroup .outlineFadeLink{margin:.5rem 0}}.btnContribLinks{display:inline-block;margin-top:4px;background-image:url(../img/logo-orcid.svg);background-repeat:no-repeat;background-size:1.5em auto;background-position:.5em center;padding:.5em .5em .5em 2.5em;border-radius:4px;border:1px solid #3867ce}.scielo__theme--dark .btnContribLinks{border:1px solid #86acff}.scielo__theme--light .btnContribLinks{border:1px solid #3867ce}.ModalDefault .btnContribLinks{padding:.5em .5em .5em 2.5em!important}.btnContribLinks:hover{border-color:#254895}.scielo__theme--dark .btnContribLinks:hover{border-color:#d3e0ff}.scielo__theme--light .btnContribLinks:hover{border-color:#254895}.linkGroup{position:relative;font-size:.85em}.linkGroup a.selected{position:relative}.linkGroup a.selected:after{content:'';display:block;position:absolute;bottom:-16px;left:4px;width:16px;height:7px;background:url(../img/articleContent-arrow.png) bottom center no-repeat;z-index:999}.btn-open{display:inline-block;margin:0 .625rem;transition:all .4s}@media (max-width:575.98px){.btn-open{display:block;margin:.25rem auto}}.badge{border-radius:1.5rem;font-size:.625rem;line-height:1.375rem;padding:0 .4375rem;height:1.625rem;min-width:1.625rem;letter-spacing:.5px;display:inline-block;vertical-align:text-top;border:2px solid #fff;text-align:center;text-transform:uppercase;color:#fff;background:#fff;color:#333}.scielo__theme--dark .badge{border-color:#333}.scielo__theme--light .badge{border-color:#fff}.scielo__theme--dark .badge{background-color:#c4c4c4;color:#eee}.scielo__theme--light .badge{background-color:#fff;color:#333}.badge-light,.badge-primary{background:#3867ce;color:#fff}.scielo__theme--dark .badge-light,.scielo__theme--dark .badge-primary{background-color:#86acff;color:#eee}.scielo__theme--light .badge-light,.scielo__theme--light .badge-primary{background-color:#3867ce;color:#fff}.badge-secondary{background:#fff;color:#333}.scielo__theme--dark .badge-secondary{background-color:#c4c4c4;color:#eee}.scielo__theme--light .badge-secondary{background-color:#fff;color:#333}.badge-info{background:#2195a9;color:#fff}.scielo__theme--dark .badge-info{background-color:#2299ad;color:#eee}.scielo__theme--light .badge-info{background-color:#2195a9;color:#fff}.badge-dark{background:#fff;color:#333}.scielo__theme--dark .badge-dark{background-color:#c4c4c4;color:#eee}.scielo__theme--light .badge-dark{background-color:#fff;color:#333}.badge-success{background:#2c9d45;color:#fff}.scielo__theme--dark .badge-success{background-color:#2c9d45;color:#eee}.scielo__theme--light .badge-success{background-color:#2c9d45;color:#fff}.badge-danger{background:#c63800;color:#fff}.scielo__theme--dark .badge-danger{background-color:#ff7e4a;color:#eee}.scielo__theme--light .badge-danger{background-color:#c63800;color:#fff}.badge-warning{background:#b67f00;color:#fff}.scielo__theme--dark .badge-warning{background-color:#b67f00;color:#eee}.scielo__theme--light .badge-warning{background-color:#b67f00;color:#fff}.display-1 .badge,.display-2 .badge,.display-3 .badge,.display-4 .badge,.h1 .badge,.h1 .badge .h2 .badge,.h2 .badge,.h3 .badge,.h4 .badge,.h5 .badge,.h6 .badge,.lead,h1 .badge,h2 .badge,h3 .badge,h4 .badge,h5 .badge,h6 .badge{margin-left:-.6em}.btn+.badge{vertical-align:text-bottom;margin:0 0 1.5rem -1.3125rem;position:relative;z-index:2}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}.progress{height:.1875rem;border-radius:.25rem;font-weight:700;font-size:.75rem;line-height:1.2em;letter-spacing:.06px;color:rgba(0,0,0,.1);overflow:visible;height:1.75rem;position:relative;background-color:#efeeec}.scielo__theme--dark .progress{background-color:#414141}.scielo__theme--light .progress{background-color:#efeeec}.progress-bar{position:relative;height:1.75rem;border-radius:.25rem;color:#fff}.progress-bar~.progress-bar{border-top-left-radius:0;border-bottom-left-radius:0;margin-left:-.4375rem}.scielo__theme--dark .progress-bar{background-color:#86acff!important}.scielo__theme--light .progress-bar{background-color:#3867ce!important}.scielo__theme--dark .progress-bar.bg-primary{background-color:#86acff!important}.scielo__theme--light .progress-bar.bg-primary{background-color:#3867ce!important}.scielo__theme--dark .progress-bar.bg-info{background-color:#2299ad!important}.scielo__theme--light .progress-bar.bg-info{background-color:#2195a9!important}.scielo__theme--dark .progress-bar.bg-success{background-color:#2c9d45!important}.scielo__theme--light .progress-bar.bg-success{background-color:#2c9d45!important}.scielo__theme--dark .progress-bar.bg-warning{background-color:#b67f00!important}.scielo__theme--light .progress-bar.bg-warning{background-color:#b67f00!important}.scielo__theme--dark .progress-bar.bg-danger{background-color:#ff7e4a!important}.scielo__theme--light .progress-bar.bg-danger{background-color:#c63800!important}.img-thumbnail{display:inline-block;background:0 0;border:none;border-radius:.1875rem;padding:0;overflow:hidden}.tooltip-inner{font-size:.75rem;border-radius:.1875rem;padding:.375rem .75rem;background:#333;color:#fff}.scielo__theme--dark .tooltip-inner{background:#fff;color:#333}.scielo__theme--light .tooltip-inner{background:#333;color:#fff}.tooltip.show{opacity:1}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{border-top-color:#333}.scielo__theme--dark .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.scielo__theme--dark .bs-tooltip-top .tooltip-arrow::before{border-top-color:#fff}.scielo__theme--light .bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.scielo__theme--light .bs-tooltip-top .tooltip-arrow::before{border-top-color:#333}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{border-right-color:#333}.scielo__theme--dark .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.scielo__theme--dark .bs-tooltip-end .tooltip-arrow::before{border-right-color:#fff}.scielo__theme--light .bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.scielo__theme--light .bs-tooltip-end .tooltip-arrow::before{border-right-color:#333}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{border-bottom-color:#333}.scielo__theme--dark .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.scielo__theme--dark .bs-tooltip-bottom .tooltip-arrow::before{border-bottom-color:#fff}.scielo__theme--light .bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.scielo__theme--light .bs-tooltip-bottom .tooltip-arrow::before{border-bottom-color:#333}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{border-left-color:#333}.scielo__theme--dark .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.scielo__theme--dark .bs-tooltip-start .tooltip-arrow::before{border-left-color:#fff}.scielo__theme--light .bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.scielo__theme--light .bs-tooltip-start .tooltip-arrow::before{border-left-color:#333}.scielo__loading-block{display:block;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:6.25rem;height:6.25rem;opacity:0;transition:opacity .5s linear;transition-delay:.5s;z-index:1050}.scielo__loading-backdrop{transition:opacity .5s linear;opacity:0;position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:1040}.scielo__loading-backdrop--dark{background:#da202c}.scielo__loading-backdrop--light{background:#fff}.scielo__loading-visible .scielo__loading-backdrop,.scielo__loading-visible .scielo__loading-block{opacity:1}.scielo__loading-hide .scielo__loading-backdrop{transition-delay:.7s}.scielo__loading-hide .scielo__loading-block{transition-delay:0}.scielo__loading-inline{position:relative;display:inline-block;width:1.25rem;height:1.25rem}.scielo__loading-inline:before{content:'';box-sizing:border-box;position:absolute;top:50%;left:50%;border-radius:50%;border:2px solid rgba(0,176,230,.2);border-top-color:#fff;animation:spinner .8s linear infinite;width:1.25rem;height:1.25rem;margin-top:-.625rem;margin-left:-.625rem}.scielo__theme--dark .scielo__loading-inline:before{border-color:rgba(0,176,230,.6);border-top-color:#eee}.scielo__theme--light .scielo__loading-inline:before{border-color:rgba(0,176,230,.2);border-top-color:#fff}[class*=b3__btn-with-icon] .scielo__loading-inline:before{border-color:#414141;border-top-color:#fff}.scielo__theme--dark [class*=b3__btn-with-icon] .scielo__loading-inline:before{border-color:#414141;border-top-color:#fff}[class*=b3__btn-with-icon--left] .scielo__loading-inline:before{padding-left:2rem}[class*=b3__btn-with-icon--left] .scielo__loading-inline:before [class^=material-icons]{position:absolute;top:50%;transform:translateY(-50%);width:1.25rem;height:1.25rem;font-size:1.25rem;line-height:1.25rem}[class*=b3__btn-with-icon--left] .scielo__loading-inline:before [class^=material-icons]:before{vertical-align:top}[class*=b3__btn-with-icon--left] .scielo__loading-inline:before [class^=material-icons]{left:.5rem}[class*=b3__btn-with-icon--right] .scielo__loading-inline:before{padding-right:2rem}[class*=b3__btn-with-icon--right] .scielo__loading-inline:before [class^=material-icons]{position:absolute;top:50%;transform:translateY(-50%);width:1.25rem;height:1.25rem;font-size:1.25rem;line-height:1.25rem}[class*=b3__btn-with-icon--right] .scielo__loading-inline:before [class^=material-icons]:before{vertical-align:top}[class*=b3__btn-with-icon--right] .scielo__loading-inline:before [class^=material-icons]{right:.5rem}.btn-group-lg>.btn .scielo__loading-inline,.btn-lg .scielo__loading-inline{width:1.5rem;height:1.5rem}.btn-group-lg>.btn .scielo__loading-inline:before,.btn-lg .scielo__loading-inline:before{width:1.5rem;height:1.5rem;margin-top:-.75rem;margin-left:-.75rem}@keyframes spinner{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:top;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1.2em;height:1.2em;border-width:.13em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1.2em;height:1.2em}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{animation-duration:1.5s}}[class*=" sci-ico-"],[class^=sci-ico-]{display:inline-block;font-size:24px;height:24px;line-height:1}.article-title [class*=" sci-ico-"],.article-title [class^=sci-ico-]{float:none}.article [class*=" sci-ico-"],.article [class^=sci-ico-]{vertical-align:text-bottom;margin-right:3px}.modal-body [class*=" sci-ico-"],.modal-body [class^=sci-ico-]{margin-right:0}[class*=" sci-ico-"]:before,[class^=sci-ico-]:before{font-family:'Material Icons Outlined';color:inherit}.sci-ico-top:before{content:"vertical_align_top"}.sci-ico-home:before{content:"home"}.sci-ico-zoom:before{content:"zoom_in"}.sci-ico-translation:before{content:"translate"}.sci-ico-socialEmail:before,.sci-ico-socialFacebook:before,.sci-ico-socialGooglePlus:before,.sci-ico-socialOther:before,.sci-ico-socialTwitter:before,.sci-ico-socialTwitterSingle:before{content:"share"}.sci-ico-similar:before{content:"playlist_add"}.sci-ico-newWindow:before{content:"open_in_new"}.sci-ico-metrics:before{content:"show_chart"}.sci-ico-citation:before,.sci-ico-link:before{content:"link"}.sci-ico-home:before{content:"home"}.sci-ico-floatingMenuDefault:before{content:"more_horiz"}.sci-ico-floatingMenuClose:before{content:"close"}.sci-ico-download:before,.sci-ico-fileCSV:before,.sci-ico-fileEPUB:before,.sci-ico-filePDF:before,.sci-ico-fileXML:before{content:"file_download"}.sci-ico-fileTable:before{content:"table_chart"}.sci-ico-fileFormula:before{content:"functions"}.sci-ico-figures:before,.sci-ico-fileFigure:before{content:"image"}.sci-ico-email:before,.sci-ico-emailOutlined:before{content:"email"}.sci-ico-arrowUp:before{content:"keyboard_arrow_up"}.sci-ico-arrowRight:before{content:"keyboard_arrow_right"}.sci-ico-arrowLeft:before{content:"keyboard_arrow_left"}.sci-ico-arrowDown:before{content:"keyboard_arrow_down"}.sci-ico-socialRSS:before{content:"rss_feed"}.sci-ico-pin:before{content:"location_on"}.sci-ico-copy:before{content:"content_copy"}.sci-ico-authorInstruction:before{content:"help_outline"}.sci-ico-about:before{content:"info"}.sci-ico-check:before{content:"check"}.sci-ico-top:before{content:"vertical_align_top"}.sci-ico-cc,.sci-ico-cc-by,.sci-ico-cc-nc,.sci-ico-cc-nd,.sci-ico-cc-sa,.sci-ico-cr,.sci-ico-public-domain{display:none}.scielo__sidenav__bottom-menu{display:none}@media screen and (min-width:768px){.scielo__sidenav__bottom-menu{display:block;position:fixed;transition:.5s all;left:0;bottom:0;width:16.875rem}.scielo__theme--dark .scielo__sidenav__bottom-menu{background:#414141}.scielo__sidenav__bottom-menu ul{width:16.875rem}.scielo__sidenav__bottom-menu .scielo__ico--double_arrow_left:before{transition:.25s all}}@media screen and (min-width:768px){.scielo__sidenav__header{display:grid;grid-template-columns:16.875rem auto;transition:.5s all}}.scielo__sidenav__header .scielo__sidenav__toggle{position:absolute;right:16px;top:50%;transform:translateY(-50%);padding-left:1rem;padding-right:1rem}.scielo__sidenav__header-brand,.scielo__sidenav__header-site{position:relative;padding:.75rem 1rem}.scielo__sidenav__header-brand{line-height:2.25rem;padding-left:1.3125rem}.scielo__sidenav__header-brand .scielo__logo--small{vertical-align:middle}@media screen and (min-width:768px){.scielo__sidenav__header-brand{position:fixed;top:0;z-index:2;transition:.5s all;grid-column:1;width:16.875rem;line-height:3rem}}@media screen and (min-width:768px){.scielo__sidenav__header-site{transition:.5s all;grid-column:2;display:grid;grid-template-columns:35% auto;grid-template-rows:3rem;border-bottom:1px solid #efeeec;padding-left:1.5rem;padding-right:1.5rem}}@media screen and (min-width:992px){.scielo__sidenav__header-site{grid-template-columns:45% auto;padding-left:2rem;padding-right:2rem}}@media screen and (min-width:1200px){.scielo__sidenav__header-site{padding-left:2.5rem;padding-right:2.5rem}}.scielo__sidenav__header-functions{display:grid;grid-template-columns:80% 20%;grid-template-areas:"a b" "c c"}.scielo__sidenav__header-functions .btn{margin-bottom:0}.scielo__sidenav__header-functions .btn+.badge{margin-bottom:.2rem;margin-left:-1.8rem;pointer-events:none}.scielo__sidenav__header-functions .input-group.is-search input{max-width:10.625rem}@media screen and (min-width:768px){.scielo__sidenav__header-functions .input-group.is-search input{max-width:100%}}@media screen and (min-width:768px){.scielo__sidenav__header-functions{grid-column:2;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));grid-gap:.75rem;grid-template-areas:"c b a"}}.scielo__sidenav__header-functions__item{grid-area:a;white-space:nowrap}.scielo__sidenav__header-functions__item--small{grid-area:b;text-align:right;white-space:nowrap}.scielo__sidenav__header-functions__item--large{grid-area:c;white-space:nowrap}.scielo__sidenav__header-title{font-size:1.125rem;color:#c4c4c4;border-bottom:1px solid #414141;padding-bottom:.75rem;margin-bottom:1.5rem;margin-left:-1rem;margin-right:-1rem;padding-left:1rem;padding-right:1rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}@media screen and (min-width:768px){.scielo__sidenav__header-title{grid-column:1;margin:0;padding:0;color:#333;font-weight:700;font-size:1.3125rem;line-height:1.25em;letter-spacing:0;font-weight:400;line-height:3rem;border-bottom:none}}@media screen and (min-width:768px){.scielo__sidenav__menu{position:fixed;left:0;top:4.5rem;bottom:3rem;transition:.5s all;overflow-y:auto;overflow-x:hidden;width:16.875rem}.scielo__sidenav__menu ul{width:16.875rem}.scielo__sidenav__menu-item{transition:.25s all;transition-delay:.5s;opacity:1;display:inline-block}}@media screen and (max-width:767px){.scielo__theme--dark .scielo__sidenav{border-bottom:1px solid #393939}}@media screen and (min-width:768px){.scielo__theme--dark .scielo__sidenav__bottom-menu,.scielo__theme--dark .scielo__sidenav__menu{border-right:1px solid #393939}}@media screen and (min-width:768px){.scielo__theme--dark .scielo__sidenav__header-brand,.scielo__theme--dark .scielo__sidenav__header-site{border-bottom:1px solid #393939}}@media screen and (max-width:767px){.scielo__theme--dark .scielo__sidenav__header-title{background:#333;color:#c4c4c4;border-bottom:1px solid #414141;margin-top:-.8rem;padding-top:.8rem}}@media screen and (max-width:767px){.scielo__sidenav+.container{padding-top:5.25rem}}@media screen and (min-width:768px){.scielo__sidenav+.container{transition:.5s all;padding-left:18.375rem;padding-right:1.5rem;max-width:100%!important}}@media screen and (min-width:992px){.scielo__sidenav+.container{padding-left:18.875rem;padding-right:2rem}}@media screen and (min-width:1200px){.scielo__sidenav+.container{padding-left:19.375rem;padding-right:2.5rem}}@media screen and (max-width:767px){.scielo__sidenav{position:fixed;top:0;width:100%;height:3.75rem;overflow:hidden;transition:.5s;z-index:9}.scielo__sidenav--opened{height:100vh;overflow:auto}.scielo__sidenav--opened .scielo__sidenav__toggle-text--closed{display:none}.scielo__sidenav--opened .scielo__sidenav__header .scielo__sidenav__toggle{padding:0;width:2rem}.scielo__sidenav--opened .scielo__sidenav__header .scielo__sidenav__toggle [class^=material-icons]{position:absolute;top:50%;transform:translateY(-50%);width:1.25rem;height:1.25rem;font-size:1.25rem;line-height:1.25rem}.scielo__sidenav--opened .scielo__sidenav__header .scielo__sidenav__toggle [class^=material-icons]:before{vertical-align:top}.scielo__sidenav--opened .scielo__sidenav__header .scielo__sidenav__toggle [class^=material-icons]{top:50%;left:50%;transform:translate(-50%,-50%)}}@media screen and (min-width:768px){.scielo__sidenav--minimized .scielo__sidenav__bottom-menu,.scielo__sidenav--minimized .scielo__sidenav__header-brand,.scielo__sidenav--minimized .scielo__sidenav__menu{overflow-x:hidden;width:4.5rem}.scielo__sidenav--minimized .scielo__sidenav__header{grid-template-columns:4.5rem auto}.scielo__sidenav--minimized .scielo__sidenav__bottom-menu .scielo__ico--double_arrow_left:before{transform:rotate(180deg)}.scielo__sidenav--minimized .scielo__sidenav__menu-item{opacity:0}}@media screen and (min-width:768px) and (min-width:768px){.scielo__sidenav--minimized+.container{padding-left:6rem}}@media screen and (min-width:768px) and (min-width:992px){.scielo__sidenav--minimized+.container{padding-left:6.5rem}}@media screen and (min-width:768px) and (min-width:1200px){.scielo__sidenav--minimized+.container{padding-left:7rem}}.scielo__text-color--light{color:#333!important}.scielo__text-color--dark{color:#c4c4c4!important}.scielo__text-color__emphasis--light{color:#00314c!important}.scielo__text-color__emphasis--dark{color:#eee!important}.scielo__text-color__subtle--light{color:#6c6b6b!important}.scielo__text-color__subtle--dark{color:#adadad!important}.scielo__text-color__menu--light{color:#fff!important}.scielo__text-color__menu--dark{color:#eee!important}.scielo__text-color__interaction--light{color:#3867ce!important}.scielo__text-color__interaction--dark{color:#86acff!important}.scielo__text-color__positive--light{color:#2c9d45!important}.scielo__text-color__positive--dark{color:#2c9d45!important}.scielo__text-color__negative--light{color:#c63800!important}.scielo__text-color__negative--dark{color:#ff7e4a!important}.scielo__bg__gray--1{background-color:#f7f6f4!important}.scielo__bg__gray--2{background-color:#efeeec!important}.scielo__bg__white--1{background-color:#393939!important}.scielo__bg__white--2{background-color:#414141!important}.scielo__border-top{border-top:2px solid #fff!important}.scielo__theme--dark .scielo__border-top{border-top-color:#eee!important}.scielo__theme--light .scielo__border-top{border-top-color:#fff!important}.scielo__border-bottom{border-bottom:2px solid #fff!important}.scielo__theme--dark .scielo__border-bottom{border-bottom-color:#eee!important}.scielo__theme--light .scielo__border-bottom{border-bottom-color:#fff!important}.scielo__padding-top{padding-top:1.5rem}.scielo__padding-top--small{padding-top:.75rem}.scielo__padding-top--large{padding-top:3rem}.scielo__padding-top--none{padding-top:0}.scielo__padding-bottom{padding-bottom:1.5rem}.scielo__padding-bottom--small{padding-bottom:.75rem}.scielo__padding-bottom--large{padding-bottom:3rem}.scielo__padding-bottom--none{padding-bottom:0}.scielo__padding-top-bottom{padding-top:1.5rem;padding-bottom:1.5rem}.scielo__padding-top-bottom--small{padding-top:.75rem;padding-bottom:.75rem}.scielo__padding-top-bottom--large{padding-top:3rem;padding-bottom:3rem}.scielo__padding-top-bottom--none{padding-top:0;padding-bottom:0}.scielo__padding-left{padding-left:1.5rem}.scielo__padding-left--small{padding-left:.75rem}.scielo__padding-left--large{padding-left:3rem}.scielo__padding-left--none{padding-left:0}.scielo__padding-right{padding-right:1.5rem}.scielo__padding-right--small{padding-right:.75rem}.scielo__padding-right--large{padding-right:3rem}.scielo__padding-right--none{padding-right:0}.scielo__padding-left-right{padding-left:1.5rem;padding-right:1.5rem}.scielo__padding-left-right--small{padding-left:.75rem;padding-right:.75rem}.scielo__padding-left-right--large{padding-left:3rem;padding-right:3rem}.scielo__padding-left-right--none{padding-left:0;padding-right:0}.scielo__margin-top{margin-top:1.5rem!important}.scielo__margin-top--small{margin-top:.75rem!important}.scielo__margin-top--large{margin-top:3rem!important}.scielo__margin-top--none{margin-top:0}.scielo__margin-bottom{margin-bottom:1.5rem!important}.scielo__margin-bottom--small{margin-bottom:.75rem!important}.scielo__margin-bottom--large{margin-bottom:3rem!important}.scielo__margin-bottom--none{margin-bottom:0}.scielo__margin-top-bottom{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.scielo__margin-top-bottom--small{margin-top:.75rem!important;margin-bottom:.75rem!important}.scielo__margin-top-bottom--large{margin-top:3rem!important;margin-bottom:3rem!important}.scielo__margin-top-bottom--none{margin-top:0!important;margin-bottom:0!important}.scielo__margin-left{margin-left:1.5rem!important}.scielo__margin-left--small{margin-left:.75rem!important}.scielo__margin-left--large{margin-left:3rem!important}.scielo__margin-left--none{margin-left:0!important}.scielo__margin-right{margin-right:1.5rem!important}.scielo__margin-right--small{margin-right:.75rem!important}.scielo__margin-right--large{margin-right:3rem!important}.scielo__margin-right--none{margin-right:0!important}.scielo__margin-left-right{margin-left:1.5rem!important;margin-right:1.5rem!important}.scielo__margin-left-right--small{margin-left:.75rem!important;margin-right:.75rem!important}.scielo__margin-left-right--large{margin-left:3rem!important;margin-right:3rem!important}.scielo__margin-left-right--none{margin-left:0!important;margin-right:0!important}@media print{.badge,.breadcrumb,.navbar.sticky-top,.scielo__levelMenu,.scielo__logo-scielo--small,.scielo__logo-scielo-caption,.scielo__logo-scielo-collection,.scielo__search-articles{-webkit-print-color-adjust:exact!important}#flDebugToolbarHandle{display:none!important}body.journal{padding:0!important}.fixed-top{position:static!important}.collection-news,.journalContent,.table-journal-list tr{break-inside:avoid!important}.scielo__shadow-2{box-shadow:none!important;border-bottom:1px solid #ddd}.bd-example .row .col-sm-9{flex:0 0 auto;width:100%}} +/*# sourceMappingURL=bootstrap.css.map */ \ No newline at end of file diff --git a/manuscripts/static/jats/jats-preview.css b/manuscripts/static/jats/jats-preview.css new file mode 100644 index 0000000..b65ee5c --- /dev/null +++ b/manuscripts/static/jats/jats-preview.css @@ -0,0 +1,216 @@ +/* Stylesheet for NLM/NCBI Journal Publishing 3.0 Preview HTML + January 2009 + + ~~~~~~~~~~~~~~ + National Center for Biotechnology Information (NCBI) + National Library of Medicine (NLM) + ~~~~~~~~~~~~~~ + +This work is in the public domain and may be reproduced, published or +otherwise used without the permission of the National Library of Medicine (NLM). + +We request only that the NLM is cited as the source of the work. + +Although all reasonable efforts have been taken to ensure the accuracy and +reliability of the software and data, the NLM and the U.S. Government do +not and cannot warrant the performance or results that may be obtained by +using this software or data. The NLM and the U.S. Government disclaim all +warranties, express or implied, including warranties of performance, +merchantability or fitness for any particular purpose. + +*/ + + +/* --------------- Page setup ------------------------ */ + +/* page and text defaults */ + +body { margin-left: 8%; + margin-right: 8%; + background-color: #f8f8f8 } + + +div > *:first-child { margin-top:0em } + +div { margin-top: 0.5em } + +div.front, div.footer { } + +.back, .body { font-family: serif } + +div.metadata { font-family: sans-serif } +div.centered { text-align: center } + +div.table { display: table } +div.metadata.table { width: 100% } +div.row { display: table-row } +div.cell { display: table-cell; padding-left: 0.25em; padding-right: 0.25em } + +div.metadata div.cell { + vertical-align: top } + +div.two-column div.cell { + width: 50% } + +div.one-column div.cell.spanning { width: 100% } + +div.metadata-group { margin-top: 0.5em; + font-size: 75% } + +div.metadata-group > p, div.metadata-group > div { margin-top: 0.5em } + +div.metadata-area * { margin: 0em } + +div.metadata-chunk { margin-left: 1em } + +div.branding { text-align: center } + +div.document-title-notes { + text-align: center; + width: 60%; + margin-left: auto; + margin-right: auto + } + +div.footnote { font-size: 90% } + +/* rules */ +hr.part-rule { + border: thin solid black; + width: 50%; + margin-top: 1em; + margin-bottom: 1em; + } + +hr.section-rule { + border: thin dotted black; + width: 50%; + margin-top: 1em; + margin-bottom: 1em; + } + +/* superior numbers that are cross-references */ +.xref { + color: red; + } + +/* generated text */ +.generated { color: gray; } + +.warning, tex-math { + font-size:80%; font-family: sans-serif } + +.warning { + color: red } + +.tex-math { color: green } + +.data { + color: black; + } + +.formula { + font-family: sans-serif; + font-size: 90% } + +/* --------------- Titling levels -------------------- */ + + +h1, h2, h3, h4, h5, h6 { + display: block; + margin-top: 0em; + margin-bottom: 0.5em; + font-family: helvetica, sans-serif; + font-weight: bold; + color: midnightblue; + } +/* titling level 1: document title */ +.document-title { + text-align: center; + } + +/* callout titles appear in a left column (table cell) + opposite what they head */ +.callout-title { text-align: right; + margin-top: 0.5em; + margin-right: 1em; + font-size: 140% } + + + +div.section, div.back-section { + margin-top: 1em; margin-bottom: 0.5em } + +div.panel { background-color: white; + font-size: 90%; + border: thin solid black; + padding-left: 0.5em; padding-right: 0.5em; + padding-top: 0.5em; padding-bottom: 0.5em; + margin-top: 0.5em; margin-bottom: 0.5em } + +div.blockquote { font-size: 90%; + margin-left: 1em; margin-right: 1em; + margin-top: 0.5em; margin-bottom: 0.5em } + +div.caption { + margin-top: 0.5em; margin-bottom: 0.5em } + +div.speech { + margin-left: 1em; margin-right: 1em; + margin-top: 0.5em; margin-bottom: 0.5em } + +div.verse-group { + margin-left: 1em; + margin-top: 0.5em; margin-bottom: 0.5em } + +div.verse-group div.verse-group { + margin-left: 1em; + margin-top: 0em; margin-bottom: 0em } + +div.note { margin-top: 0em; margin-left: 1em; + font-size: 85% } + +.ref-label { margin-top: 0em; vertical-align: top } + +.ref-content { margin-top: 0em; padding-left: 0.25em } + +h5.label { margin-top: 0em; margin-bottom: 0em } + +p { margin-top: 0.5em; margin-bottom: 0em } + +p.first { margin-top: 0em } + +p.verse-line, p.citation { margin-top: 0em; margin-bottom: 0em; margin-left: 2em; text-indent: -2em } + +p.address-line { margin-top: 0em; margin-bottom: 0em; margin-left: 2em } + +ul, ol { margin-top: 0.5em } + +li { margin-top: 0.5em; margin-bottom: 0em } +li > p { margin-top: 0.2em; margin-bottom: 0em } + +div.def-list { border-spacing: 0.25em } + +div.def-list div.cell { vertical-align: top; + border-bottom: thin solid black; + padding-bottom: 0.5em } + +div.def-list div.def-list-head { + text-align: left } + +/* text decoration */ +.label { font-weight: bold; font-family: sans-serif; font-size: 80% } + +.monospace { + font-family: monospace; + } + +.overline{ + text-decoration: overline; + } + +a { text-decoration: none } +a:hover { text-decoration: underline } + +/* ---------------- End ------------------------------ */ + diff --git a/manuscripts/static/js/scielo-bundle-min.js b/manuscripts/static/js/scielo-bundle-min.js new file mode 100644 index 0000000..655d26c --- /dev/null +++ b/manuscripts/static/js/scielo-bundle-min.js @@ -0,0 +1,2 @@ +!function(t,e){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=t.document?e(t,!0):function(t){if(t.document)return e(t);throw new Error("jQuery requires a window with a document")}:e(t)}("undefined"!=typeof window?window:this,function(_,N){"use strict";function v(t){return"function"==typeof t&&"number"!=typeof t.nodeType&&"function"!=typeof t.item}function m(t){return null!=t&&t===t.window}var e=[],I=Object.getPrototypeOf,a=e.slice,H=e.flat?function(t){return e.flat.call(t)}:function(t){return e.concat.apply([],t)},F=e.push,R=e.indexOf,q={},Y=q.toString,W=q.hasOwnProperty,B=W.toString,z=B.call(Object),g={},k=_.document,U={type:!0,src:!0,nonce:!0,noModule:!0};function G(t,e,i){var n,o,s=(i=i||k).createElement("script");if(s.text=t,e)for(n in U)(o=e[n]||e.getAttribute&&e.getAttribute(n))&&s.setAttribute(n,o);i.head.appendChild(s).parentNode.removeChild(s)}function f(t){return null==t?t+"":"object"==typeof t||"function"==typeof t?q[Y.call(t)]||"object":typeof t}var t="3.6.3",x=function(t,e){return new x.fn.init(t,e)};function V(t){var e=!!t&&"length"in t&&t.length,i=f(t);return!v(t)&&!m(t)&&("array"===i||0===e||"number"==typeof e&&0<e&&e-1 in t)}x.fn=x.prototype={jquery:t,constructor:x,length:0,toArray:function(){return a.call(this)},get:function(t){return null==t?a.call(this):t<0?this[t+this.length]:this[t]},pushStack:function(t){t=x.merge(this.constructor(),t);return t.prevObject=this,t},each:function(t){return x.each(this,t)},map:function(i){return this.pushStack(x.map(this,function(t,e){return i.call(t,e,t)}))},slice:function(){return this.pushStack(a.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(x.grep(this,function(t,e){return(e+1)%2}))},odd:function(){return this.pushStack(x.grep(this,function(t,e){return e%2}))},eq:function(t){var e=this.length,t=+t+(t<0?e:0);return this.pushStack(0<=t&&t<e?[this[t]]:[])},end:function(){return this.prevObject||this.constructor()},push:F,sort:e.sort,splice:e.splice},x.extend=x.fn.extend=function(){var t,e,i,n,o,s=arguments[0]||{},r=1,a=arguments.length,l=!1;for("boolean"==typeof s&&(l=s,s=arguments[r]||{},r++),"object"==typeof s||v(s)||(s={}),r===a&&(s=this,r--);r<a;r++)if(null!=(t=arguments[r]))for(e in t)i=t[e],"__proto__"!==e&&s!==i&&(l&&i&&(x.isPlainObject(i)||(n=Array.isArray(i)))?(o=s[e],o=n&&!Array.isArray(o)?[]:n||x.isPlainObject(o)?o:{},n=!1,s[e]=x.extend(l,o,i)):void 0!==i&&(s[e]=i));return s},x.extend({expando:"jQuery"+(t+Math.random()).replace(/\D/g,""),isReady:!0,error:function(t){throw new Error(t)},noop:function(){},isPlainObject:function(t){return!(!t||"[object Object]"!==Y.call(t)||(t=I(t))&&("function"!=typeof(t=W.call(t,"constructor")&&t.constructor)||B.call(t)!==z))},isEmptyObject:function(t){for(var e in t)return!1;return!0},globalEval:function(t,e,i){G(t,{nonce:e&&e.nonce},i)},each:function(t,e){var i,n=0;if(V(t))for(i=t.length;n<i&&!1!==e.call(t[n],n,t[n]);n++);else for(n in t)if(!1===e.call(t[n],n,t[n]))break;return t},makeArray:function(t,e){e=e||[];return null!=t&&(V(Object(t))?x.merge(e,"string"==typeof t?[t]:t):F.call(e,t)),e},inArray:function(t,e,i){return null==e?-1:R.call(e,t,i)},merge:function(t,e){for(var i=+e.length,n=0,o=t.length;n<i;n++)t[o++]=e[n];return t.length=o,t},grep:function(t,e,i){for(var n=[],o=0,s=t.length,r=!i;o<s;o++)!e(t[o],o)!=r&&n.push(t[o]);return n},map:function(t,e,i){var n,o,s=0,r=[];if(V(t))for(n=t.length;s<n;s++)null!=(o=e(t[s],s,i))&&r.push(o);else for(s in t)null!=(o=e(t[s],s,i))&&r.push(o);return H(r)},guid:1,support:g}),"function"==typeof Symbol&&(x.fn[Symbol.iterator]=e[Symbol.iterator]),x.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(t,e){q["[object "+e+"]"]=e.toLowerCase()});function n(t,e,i){for(var n=[],o=void 0!==i;(t=t[e])&&9!==t.nodeType;)if(1===t.nodeType){if(o&&x(t).is(i))break;n.push(t)}return n}function X(t,e){for(var i=[];t;t=t.nextSibling)1===t.nodeType&&t!==e&&i.push(t);return i}var t=function(N){function u(t,e){return t="0x"+t.slice(1)-65536,e||(t<0?String.fromCharCode(65536+t):String.fromCharCode(t>>10|55296,1023&t|56320))}function I(t,e){return e?"\0"===t?"�":t.slice(0,-1)+"\\"+t.charCodeAt(t.length-1).toString(16)+" ":"\\"+t}function H(){k()}var t,h,w,s,F,p,R,q,_,l,c,k,x,i,T,f,n,o,m,S="sizzle"+ +new Date,d=N.document,C=0,Y=0,W=O(),B=O(),z=O(),g=O(),U=function(t,e){return t===e&&(c=!0),0},G={}.hasOwnProperty,e=[],V=e.pop,X=e.push,E=e.push,Z=e.slice,v=function(t,e){for(var i=0,n=t.length;i<n;i++)if(t[i]===e)return i;return-1},Q="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",r="[\\x20\\t\\r\\n\\f]",a="(?:\\\\[\\da-fA-F]{1,6}"+r+"?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+",J="\\["+r+"*("+a+")(?:"+r+"*([*^$|!~]?=)"+r+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+a+"))|)"+r+"*\\]",K=":("+a+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+J+")*)|.*)\\)|)",tt=new RegExp(r+"+","g"),y=new RegExp("^"+r+"+|((?:^|[^\\\\])(?:\\\\.)*)"+r+"+$","g"),et=new RegExp("^"+r+"*,"+r+"*"),it=new RegExp("^"+r+"*([>+~]|"+r+")"+r+"*"),nt=new RegExp(r+"|>"),ot=new RegExp(K),st=new RegExp("^"+a+"$"),b={ID:new RegExp("^#("+a+")"),CLASS:new RegExp("^\\.("+a+")"),TAG:new RegExp("^("+a+"|[*])"),ATTR:new RegExp("^"+J),PSEUDO:new RegExp("^"+K),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+r+"*(even|odd|(([+-]|)(\\d*)n|)"+r+"*(?:([+-]|)"+r+"*(\\d+)|))"+r+"*\\)|)","i"),bool:new RegExp("^(?:"+Q+")$","i"),needsContext:new RegExp("^"+r+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+r+"*((?:-\\d)?\\d*)"+r+"*\\)|)(?=[^-]|$)","i")},rt=/HTML$/i,at=/^(?:input|select|textarea|button)$/i,lt=/^h\d$/i,D=/^[^{]+\{\s*\[native \w/,ct=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,dt=/[+~]/,A=new RegExp("\\\\[\\da-fA-F]{1,6}"+r+"?|\\\\([^\\r\\n\\f])","g"),ut=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ht=vt(function(t){return!0===t.disabled&&"fieldset"===t.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{E.apply(e=Z.call(d.childNodes),d.childNodes),e[d.childNodes.length].nodeType}catch(t){E={apply:e.length?function(t,e){X.apply(t,Z.call(e))}:function(t,e){for(var i=t.length,n=0;t[i++]=e[n++];);t.length=i-1}}}function L(e,t,i,n){var o,s,r,a,l,c,d=t&&t.ownerDocument,u=t?t.nodeType:9;if(i=i||[],"string"!=typeof e||!e||1!==u&&9!==u&&11!==u)return i;if(!n&&(k(t),t=t||x,T)){if(11!==u&&(a=ct.exec(e)))if(o=a[1]){if(9===u){if(!(c=t.getElementById(o)))return i;if(c.id===o)return i.push(c),i}else if(d&&(c=d.getElementById(o))&&m(t,c)&&c.id===o)return i.push(c),i}else{if(a[2])return E.apply(i,t.getElementsByTagName(e)),i;if((o=a[3])&&h.getElementsByClassName&&t.getElementsByClassName)return E.apply(i,t.getElementsByClassName(o)),i}if(h.qsa&&!g[e+" "]&&(!f||!f.test(e))&&(1!==u||"object"!==t.nodeName.toLowerCase())){if(c=e,d=t,1===u&&(nt.test(e)||it.test(e))){for((d=dt.test(e)&>(t.parentNode)||t)===t&&h.scope||((r=t.getAttribute("id"))?r=r.replace(ut,I):t.setAttribute("id",r=S)),s=(l=p(e)).length;s--;)l[s]=(r?"#"+r:":scope")+" "+P(l[s]);c=l.join(",")}try{if(h.cssSupportsSelector&&!CSS.supports("selector(:is("+c+"))"))throw new Error;return E.apply(i,d.querySelectorAll(c)),i}catch(t){g(e,!0)}finally{r===S&&t.removeAttribute("id")}}}return q(e.replace(y,"$1"),t,i,n)}function O(){var i=[];function n(t,e){return i.push(t+" ")>w.cacheLength&&delete n[i.shift()],n[t+" "]=e}return n}function $(t){return t[S]=!0,t}function M(t){var e=x.createElement("fieldset");try{return!!t(e)}catch(t){return!1}finally{e.parentNode&&e.parentNode.removeChild(e)}}function pt(t,e){for(var i=t.split("|"),n=i.length;n--;)w.attrHandle[i[n]]=e}function ft(t,e){var i=e&&t,n=i&&1===t.nodeType&&1===e.nodeType&&t.sourceIndex-e.sourceIndex;if(n)return n;if(i)for(;i=i.nextSibling;)if(i===e)return-1;return t?1:-1}function mt(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ht(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function j(r){return $(function(s){return s=+s,$(function(t,e){for(var i,n=r([],t.length,s),o=n.length;o--;)t[i=n[o]]&&(t[i]=!(e[i]=t[i]))})})}function gt(t){return t&&void 0!==t.getElementsByTagName&&t}for(t in h=L.support={},F=L.isXML=function(t){var e=t&&t.namespaceURI,t=t&&(t.ownerDocument||t).documentElement;return!rt.test(e||t&&t.nodeName||"HTML")},k=L.setDocument=function(t){var t=t?t.ownerDocument||t:d;return t!=x&&9===t.nodeType&&t.documentElement&&(i=(x=t).documentElement,T=!F(x),d!=x&&(t=x.defaultView)&&t.top!==t&&(t.addEventListener?t.addEventListener("unload",H,!1):t.attachEvent&&t.attachEvent("onunload",H)),h.scope=M(function(t){return i.appendChild(t).appendChild(x.createElement("div")),void 0!==t.querySelectorAll&&!t.querySelectorAll(":scope fieldset div").length}),h.cssSupportsSelector=M(function(){return CSS.supports("selector(*)")&&x.querySelectorAll(":is(:jqfake)")&&!CSS.supports("selector(:is(*,:jqfake))")}),h.attributes=M(function(t){return t.className="i",!t.getAttribute("className")}),h.getElementsByTagName=M(function(t){return t.appendChild(x.createComment("")),!t.getElementsByTagName("*").length}),h.getElementsByClassName=D.test(x.getElementsByClassName),h.getById=M(function(t){return i.appendChild(t).id=S,!x.getElementsByName||!x.getElementsByName(S).length}),h.getById?(w.filter.ID=function(t){var e=t.replace(A,u);return function(t){return t.getAttribute("id")===e}},w.find.ID=function(t,e){if(void 0!==e.getElementById&&T)return(e=e.getElementById(t))?[e]:[]}):(w.filter.ID=function(t){var e=t.replace(A,u);return function(t){t=void 0!==t.getAttributeNode&&t.getAttributeNode("id");return t&&t.value===e}},w.find.ID=function(t,e){if(void 0!==e.getElementById&&T){var i,n,o,s=e.getElementById(t);if(s){if((i=s.getAttributeNode("id"))&&i.value===t)return[s];for(o=e.getElementsByName(t),n=0;s=o[n++];)if((i=s.getAttributeNode("id"))&&i.value===t)return[s]}return[]}}),w.find.TAG=h.getElementsByTagName?function(t,e){return void 0!==e.getElementsByTagName?e.getElementsByTagName(t):h.qsa?e.querySelectorAll(t):void 0}:function(t,e){var i,n=[],o=0,s=e.getElementsByTagName(t);if("*"!==t)return s;for(;i=s[o++];)1===i.nodeType&&n.push(i);return n},w.find.CLASS=h.getElementsByClassName&&function(t,e){if(void 0!==e.getElementsByClassName&&T)return e.getElementsByClassName(t)},n=[],f=[],(h.qsa=D.test(x.querySelectorAll))&&(M(function(t){var e;i.appendChild(t).innerHTML="<a id='"+S+"'></a><select id='"+S+"-\r\\' msallowcapture=''><option selected=''></option></select>",t.querySelectorAll("[msallowcapture^='']").length&&f.push("[*^$]="+r+"*(?:''|\"\")"),t.querySelectorAll("[selected]").length||f.push("\\["+r+"*(?:value|"+Q+")"),t.querySelectorAll("[id~="+S+"-]").length||f.push("~="),(e=x.createElement("input")).setAttribute("name",""),t.appendChild(e),t.querySelectorAll("[name='']").length||f.push("\\["+r+"*name"+r+"*="+r+"*(?:''|\"\")"),t.querySelectorAll(":checked").length||f.push(":checked"),t.querySelectorAll("a#"+S+"+*").length||f.push(".#.+[+~]"),t.querySelectorAll("\\\f"),f.push("[\\r\\n\\f]")}),M(function(t){t.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var e=x.createElement("input");e.setAttribute("type","hidden"),t.appendChild(e).setAttribute("name","D"),t.querySelectorAll("[name=d]").length&&f.push("name"+r+"*[*^$|!~]?="),2!==t.querySelectorAll(":enabled").length&&f.push(":enabled",":disabled"),i.appendChild(t).disabled=!0,2!==t.querySelectorAll(":disabled").length&&f.push(":enabled",":disabled"),t.querySelectorAll("*,:x"),f.push(",.*:")})),(h.matchesSelector=D.test(o=i.matches||i.webkitMatchesSelector||i.mozMatchesSelector||i.oMatchesSelector||i.msMatchesSelector))&&M(function(t){h.disconnectedMatch=o.call(t,"*"),o.call(t,"[s!='']:x"),n.push("!=",K)}),h.cssSupportsSelector||f.push(":has"),f=f.length&&new RegExp(f.join("|")),n=n.length&&new RegExp(n.join("|")),t=D.test(i.compareDocumentPosition),m=t||D.test(i.contains)?function(t,e){var i=9===t.nodeType&&t.documentElement||t,e=e&&e.parentNode;return t===e||!(!e||1!==e.nodeType||!(i.contains?i.contains(e):t.compareDocumentPosition&&16&t.compareDocumentPosition(e)))}:function(t,e){if(e)for(;e=e.parentNode;)if(e===t)return!0;return!1},U=t?function(t,e){var i;return t===e?(c=!0,0):(i=!t.compareDocumentPosition-!e.compareDocumentPosition)||(1&(i=(t.ownerDocument||t)==(e.ownerDocument||e)?t.compareDocumentPosition(e):1)||!h.sortDetached&&e.compareDocumentPosition(t)===i?t==x||t.ownerDocument==d&&m(d,t)?-1:e==x||e.ownerDocument==d&&m(d,e)?1:l?v(l,t)-v(l,e):0:4&i?-1:1)}:function(t,e){if(t===e)return c=!0,0;var i,n=0,o=t.parentNode,s=e.parentNode,r=[t],a=[e];if(!o||!s)return t==x?-1:e==x?1:o?-1:s?1:l?v(l,t)-v(l,e):0;if(o===s)return ft(t,e);for(i=t;i=i.parentNode;)r.unshift(i);for(i=e;i=i.parentNode;)a.unshift(i);for(;r[n]===a[n];)n++;return n?ft(r[n],a[n]):r[n]==d?-1:a[n]==d?1:0}),x},L.matches=function(t,e){return L(t,null,null,e)},L.matchesSelector=function(t,e){if(k(t),h.matchesSelector&&T&&!g[e+" "]&&(!n||!n.test(e))&&(!f||!f.test(e)))try{var i=o.call(t,e);if(i||h.disconnectedMatch||t.document&&11!==t.document.nodeType)return i}catch(t){g(e,!0)}return 0<L(e,x,null,[t]).length},L.contains=function(t,e){return(t.ownerDocument||t)!=x&&k(t),m(t,e)},L.attr=function(t,e){(t.ownerDocument||t)!=x&&k(t);var i=w.attrHandle[e.toLowerCase()],i=i&&G.call(w.attrHandle,e.toLowerCase())?i(t,e,!T):void 0;return void 0!==i?i:h.attributes||!T?t.getAttribute(e):(i=t.getAttributeNode(e))&&i.specified?i.value:null},L.escape=function(t){return(t+"").replace(ut,I)},L.error=function(t){throw new Error("Syntax error, unrecognized expression: "+t)},L.uniqueSort=function(t){var e,i=[],n=0,o=0;if(c=!h.detectDuplicates,l=!h.sortStable&&t.slice(0),t.sort(U),c){for(;e=t[o++];)e===t[o]&&(n=i.push(o));for(;n--;)t.splice(i[n],1)}return l=null,t},s=L.getText=function(t){var e,i="",n=0,o=t.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof t.textContent)return t.textContent;for(t=t.firstChild;t;t=t.nextSibling)i+=s(t)}else if(3===o||4===o)return t.nodeValue}else for(;e=t[n++];)i+=s(e);return i},(w=L.selectors={cacheLength:50,createPseudo:$,match:b,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(t){return t[1]=t[1].replace(A,u),t[3]=(t[3]||t[4]||t[5]||"").replace(A,u),"~="===t[2]&&(t[3]=" "+t[3]+" "),t.slice(0,4)},CHILD:function(t){return t[1]=t[1].toLowerCase(),"nth"===t[1].slice(0,3)?(t[3]||L.error(t[0]),t[4]=+(t[4]?t[5]+(t[6]||1):2*("even"===t[3]||"odd"===t[3])),t[5]=+(t[7]+t[8]||"odd"===t[3])):t[3]&&L.error(t[0]),t},PSEUDO:function(t){var e,i=!t[6]&&t[2];return b.CHILD.test(t[0])?null:(t[3]?t[2]=t[4]||t[5]||"":i&&ot.test(i)&&(e=(e=p(i,!0))&&i.indexOf(")",i.length-e)-i.length)&&(t[0]=t[0].slice(0,e),t[2]=i.slice(0,e)),t.slice(0,3))}},filter:{TAG:function(t){var e=t.replace(A,u).toLowerCase();return"*"===t?function(){return!0}:function(t){return t.nodeName&&t.nodeName.toLowerCase()===e}},CLASS:function(t){var e=W[t+" "];return e||(e=new RegExp("(^|"+r+")"+t+"("+r+"|$)"))&&W(t,function(t){return e.test("string"==typeof t.className&&t.className||void 0!==t.getAttribute&&t.getAttribute("class")||"")})},ATTR:function(e,i,n){return function(t){t=L.attr(t,e);return null==t?"!="===i:!i||(t+="","="===i?t===n:"!="===i?t!==n:"^="===i?n&&0===t.indexOf(n):"*="===i?n&&-1<t.indexOf(n):"$="===i?n&&t.slice(-n.length)===n:"~="===i?-1<(" "+t.replace(tt," ")+" ").indexOf(n):"|="===i&&(t===n||t.slice(0,n.length+1)===n+"-"))}},CHILD:function(f,t,e,m,g){var y="nth"!==f.slice(0,3),v="last"!==f.slice(-4),b="of-type"===t;return 1===m&&0===g?function(t){return!!t.parentNode}:function(t,e,i){var n,o,s,r,a,l,c=y!=v?"nextSibling":"previousSibling",d=t.parentNode,u=b&&t.nodeName.toLowerCase(),h=!i&&!b,p=!1;if(d){if(y){for(;c;){for(r=t;r=r[c];)if(b?r.nodeName.toLowerCase()===u:1===r.nodeType)return!1;l=c="only"===f&&!l&&"nextSibling"}return!0}if(l=[v?d.firstChild:d.lastChild],v&&h){for(p=(a=(n=(o=(s=(r=d)[S]||(r[S]={}))[r.uniqueID]||(s[r.uniqueID]={}))[f]||[])[0]===C&&n[1])&&n[2],r=a&&d.childNodes[a];r=++a&&r&&r[c]||(p=a=0,l.pop());)if(1===r.nodeType&&++p&&r===t){o[f]=[C,a,p];break}}else if(!1===(p=h?a=(n=(o=(s=(r=t)[S]||(r[S]={}))[r.uniqueID]||(s[r.uniqueID]={}))[f]||[])[0]===C&&n[1]:p))for(;(r=++a&&r&&r[c]||(p=a=0,l.pop()))&&((b?r.nodeName.toLowerCase()!==u:1!==r.nodeType)||!++p||(h&&((o=(s=r[S]||(r[S]={}))[r.uniqueID]||(s[r.uniqueID]={}))[f]=[C,p]),r!==t)););return(p-=g)===m||p%m==0&&0<=p/m}}},PSEUDO:function(t,s){var e,r=w.pseudos[t]||w.setFilters[t.toLowerCase()]||L.error("unsupported pseudo: "+t);return r[S]?r(s):1<r.length?(e=[t,t,"",s],w.setFilters.hasOwnProperty(t.toLowerCase())?$(function(t,e){for(var i,n=r(t,s),o=n.length;o--;)t[i=v(t,n[o])]=!(e[i]=n[o])}):function(t){return r(t,0,e)}):r}},pseudos:{not:$(function(t){var n=[],o=[],a=R(t.replace(y,"$1"));return a[S]?$(function(t,e,i,n){for(var o,s=a(t,null,n,[]),r=t.length;r--;)(o=s[r])&&(t[r]=!(e[r]=o))}):function(t,e,i){return n[0]=t,a(n,null,i,o),n[0]=null,!o.pop()}}),has:$(function(e){return function(t){return 0<L(e,t).length}}),contains:$(function(e){return e=e.replace(A,u),function(t){return-1<(t.textContent||s(t)).indexOf(e)}}),lang:$(function(i){return st.test(i||"")||L.error("unsupported lang: "+i),i=i.replace(A,u).toLowerCase(),function(t){var e;do{if(e=T?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(e=e.toLowerCase())===i||0===e.indexOf(i+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var e=N.location&&N.location.hash;return e&&e.slice(1)===t.id},root:function(t){return t===i},focus:function(t){return t===x.activeElement&&(!x.hasFocus||x.hasFocus())&&!!(t.type||t.href||~t.tabIndex)},enabled:mt(!1),disabled:mt(!0),checked:function(t){var e=t.nodeName.toLowerCase();return"input"===e&&!!t.checked||"option"===e&&!!t.selected},selected:function(t){return t.parentNode&&t.parentNode.selectedIndex,!0===t.selected},empty:function(t){for(t=t.firstChild;t;t=t.nextSibling)if(t.nodeType<6)return!1;return!0},parent:function(t){return!w.pseudos.empty(t)},header:function(t){return lt.test(t.nodeName)},input:function(t){return at.test(t.nodeName)},button:function(t){var e=t.nodeName.toLowerCase();return"input"===e&&"button"===t.type||"button"===e},text:function(t){return"input"===t.nodeName.toLowerCase()&&"text"===t.type&&(null==(t=t.getAttribute("type"))||"text"===t.toLowerCase())},first:j(function(){return[0]}),last:j(function(t,e){return[e-1]}),eq:j(function(t,e,i){return[i<0?i+e:i]}),even:j(function(t,e){for(var i=0;i<e;i+=2)t.push(i);return t}),odd:j(function(t,e){for(var i=1;i<e;i+=2)t.push(i);return t}),lt:j(function(t,e,i){for(var n=i<0?i+e:e<i?e:i;0<=--n;)t.push(n);return t}),gt:j(function(t,e,i){for(var n=i<0?i+e:i;++n<e;)t.push(n);return t})}}).pseudos.nth=w.pseudos.eq,{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})w.pseudos[t]=function(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}(t);for(t in{submit:!0,reset:!0})w.pseudos[t]=function(i){return function(t){var e=t.nodeName.toLowerCase();return("input"===e||"button"===e)&&t.type===i}}(t);function yt(){}function P(t){for(var e=0,i=t.length,n="";e<i;e++)n+=t[e].value;return n}function vt(r,t,e){var a=t.dir,l=t.next,c=l||a,d=e&&"parentNode"===c,u=Y++;return t.first?function(t,e,i){for(;t=t[a];)if(1===t.nodeType||d)return r(t,e,i);return!1}:function(t,e,i){var n,o,s=[C,u];if(i){for(;t=t[a];)if((1===t.nodeType||d)&&r(t,e,i))return!0}else for(;t=t[a];)if(1===t.nodeType||d)if(o=(o=t[S]||(t[S]={}))[t.uniqueID]||(o[t.uniqueID]={}),l&&l===t.nodeName.toLowerCase())t=t[a]||t;else{if((n=o[c])&&n[0]===C&&n[1]===u)return s[2]=n[2];if((o[c]=s)[2]=r(t,e,i))return!0}return!1}}function bt(o){return 1<o.length?function(t,e,i){for(var n=o.length;n--;)if(!o[n](t,e,i))return!1;return!0}:o[0]}function wt(t,e,i,n,o){for(var s,r=[],a=0,l=t.length,c=null!=e;a<l;a++)!(s=t[a])||i&&!i(s,n,o)||(r.push(s),c&&e.push(a));return r}function _t(p,f,m,g,y,t){return g&&!g[S]&&(g=_t(g)),y&&!y[S]&&(y=_t(y,t)),$(function(t,e,i,n){var o,s,r,a=[],l=[],c=e.length,d=t||function(t,e,i){for(var n=0,o=e.length;n<o;n++)L(t,e[n],i);return i}(f||"*",i.nodeType?[i]:i,[]),u=!p||!t&&f?d:wt(d,a,p,i,n),h=m?y||(t?p:c||g)?[]:e:u;if(m&&m(u,h,i,n),g)for(o=wt(h,l),g(o,[],i,n),s=o.length;s--;)(r=o[s])&&(h[l[s]]=!(u[l[s]]=r));if(t){if(y||p){if(y){for(o=[],s=h.length;s--;)(r=h[s])&&o.push(u[s]=r);y(null,h=[],o,n)}for(s=h.length;s--;)(r=h[s])&&-1<(o=y?v(t,r):a[s])&&(t[o]=!(e[o]=r))}}else h=wt(h===e?h.splice(c,h.length):h),y?y(null,e,h,n):E.apply(e,h)})}function kt(g,y){function t(t,e,i,n,o){var s,r,a,l=0,c="0",d=t&&[],u=[],h=_,p=t||b&&w.find.TAG("*",o),f=C+=null==h?1:Math.random()||.1,m=p.length;for(o&&(_=e==x||e||o);c!==m&&null!=(s=p[c]);c++){if(b&&s){for(r=0,e||s.ownerDocument==x||(k(s),i=!T);a=g[r++];)if(a(s,e||x,i)){n.push(s);break}o&&(C=f)}v&&((s=!a&&s)&&l--,t)&&d.push(s)}if(l+=c,v&&c!==l){for(r=0;a=y[r++];)a(d,u,e,i);if(t){if(0<l)for(;c--;)d[c]||u[c]||(u[c]=V.call(n));u=wt(u)}E.apply(n,u),o&&!t&&0<u.length&&1<l+y.length&&L.uniqueSort(n)}return o&&(C=f,_=h),d}var v=0<y.length,b=0<g.length;return v?$(t):t}return yt.prototype=w.filters=w.pseudos,w.setFilters=new yt,p=L.tokenize=function(t,e){var i,n,o,s,r,a,l,c=B[t+" "];if(c)return e?0:c.slice(0);for(r=t,a=[],l=w.preFilter;r;){for(s in i&&!(n=et.exec(r))||(n&&(r=r.slice(n[0].length)||r),a.push(o=[])),i=!1,(n=it.exec(r))&&(i=n.shift(),o.push({value:i,type:n[0].replace(y," ")}),r=r.slice(i.length)),w.filter)!(n=b[s].exec(r))||l[s]&&!(n=l[s](n))||(i=n.shift(),o.push({value:i,type:s,matches:n}),r=r.slice(i.length));if(!i)break}return e?r.length:r?L.error(t):B(t,a).slice(0)},R=L.compile=function(t,e){var i,n=[],o=[],s=z[t+" "];if(!s){for(i=(e=e||p(t)).length;i--;)((s=function t(e){for(var n,i,o,s=e.length,r=w.relative[e[0].type],a=r||w.relative[" "],l=r?1:0,c=vt(function(t){return t===n},a,!0),d=vt(function(t){return-1<v(n,t)},a,!0),u=[function(t,e,i){return t=!r&&(i||e!==_)||((n=e).nodeType?c:d)(t,e,i),n=null,t}];l<s;l++)if(i=w.relative[e[l].type])u=[vt(bt(u),i)];else{if((i=w.filter[e[l].type].apply(null,e[l].matches))[S]){for(o=++l;o<s&&!w.relative[e[o].type];o++);return _t(1<l&&bt(u),1<l&&P(e.slice(0,l-1).concat({value:" "===e[l-2].type?"*":""})).replace(y,"$1"),i,l<o&&t(e.slice(l,o)),o<s&&t(e=e.slice(o)),o<s&&P(e))}u.push(i)}return bt(u)}(e[i]))[S]?n:o).push(s);(s=z(t,kt(o,n))).selector=t}return s},q=L.select=function(t,e,i,n){var o,s,r,a,l,c="function"==typeof t&&t,d=!n&&p(t=c.selector||t);if(i=i||[],1===d.length){if(2<(s=d[0]=d[0].slice(0)).length&&"ID"===(r=s[0]).type&&9===e.nodeType&&T&&w.relative[s[1].type]){if(!(e=(w.find.ID(r.matches[0].replace(A,u),e)||[])[0]))return i;c&&(e=e.parentNode),t=t.slice(s.shift().value.length)}for(o=b.needsContext.test(t)?0:s.length;o--&&(r=s[o],!w.relative[a=r.type]);)if((l=w.find[a])&&(n=l(r.matches[0].replace(A,u),dt.test(s[0].type)&>(e.parentNode)||e))){if(s.splice(o,1),t=n.length&&P(s))break;return E.apply(i,n),i}}return(c||R(t,d))(n,e,!T,i,!e||dt.test(t)&>(e.parentNode)||e),i},h.sortStable=S.split("").sort(U).join("")===S,h.detectDuplicates=!!c,k(),h.sortDetached=M(function(t){return 1&t.compareDocumentPosition(x.createElement("fieldset"))}),M(function(t){return t.innerHTML="<a href='#'></a>","#"===t.firstChild.getAttribute("href")})||pt("type|href|height|width",function(t,e,i){if(!i)return t.getAttribute(e,"type"===e.toLowerCase()?1:2)}),h.attributes&&M(function(t){return t.innerHTML="<input/>",t.firstChild.setAttribute("value",""),""===t.firstChild.getAttribute("value")})||pt("value",function(t,e,i){if(!i&&"input"===t.nodeName.toLowerCase())return t.defaultValue}),M(function(t){return null==t.getAttribute("disabled")})||pt(Q,function(t,e,i){if(!i)return!0===t[e]?e.toLowerCase():(i=t.getAttributeNode(e))&&i.specified?i.value:null}),L}(_),Z=(x.find=t,x.expr=t.selectors,x.expr[":"]=x.expr.pseudos,x.uniqueSort=x.unique=t.uniqueSort,x.text=t.getText,x.isXMLDoc=t.isXML,x.contains=t.contains,x.escapeSelector=t.escape,x.expr.match.needsContext);function l(t,e){return t.nodeName&&t.nodeName.toLowerCase()===e.toLowerCase()}var Q=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function J(t,i,n){return v(i)?x.grep(t,function(t,e){return!!i.call(t,e,t)!==n}):i.nodeType?x.grep(t,function(t){return t===i!==n}):"string"!=typeof i?x.grep(t,function(t){return-1<R.call(i,t)!==n}):x.filter(i,t,n)}x.filter=function(t,e,i){var n=e[0];return i&&(t=":not("+t+")"),1===e.length&&1===n.nodeType?x.find.matchesSelector(n,t)?[n]:[]:x.find.matches(t,x.grep(e,function(t){return 1===t.nodeType}))},x.fn.extend({find:function(t){var e,i,n=this.length,o=this;if("string"!=typeof t)return this.pushStack(x(t).filter(function(){for(e=0;e<n;e++)if(x.contains(o[e],this))return!0}));for(i=this.pushStack([]),e=0;e<n;e++)x.find(t,o[e],i);return 1<n?x.uniqueSort(i):i},filter:function(t){return this.pushStack(J(this,t||[],!1))},not:function(t){return this.pushStack(J(this,t||[],!0))},is:function(t){return!!J(this,"string"==typeof t&&Z.test(t)?x(t):t||[],!1).length}});var K,tt=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,et=((x.fn.init=function(t,e,i){if(t){if(i=i||K,"string"!=typeof t)return t.nodeType?(this[0]=t,this.length=1,this):v(t)?void 0!==i.ready?i.ready(t):t(x):x.makeArray(t,this);if(!(n="<"===t[0]&&">"===t[t.length-1]&&3<=t.length?[null,t,null]:tt.exec(t))||!n[1]&&e)return(!e||e.jquery?e||i:this.constructor(e)).find(t);if(n[1]){if(e=e instanceof x?e[0]:e,x.merge(this,x.parseHTML(n[1],e&&e.nodeType?e.ownerDocument||e:k,!0)),Q.test(n[1])&&x.isPlainObject(e))for(var n in e)v(this[n])?this[n](e[n]):this.attr(n,e[n])}else(i=k.getElementById(n[2]))&&(this[0]=i,this.length=1)}return this}).prototype=x.fn,K=x(k),/^(?:parents|prev(?:Until|All))/),it={children:!0,contents:!0,next:!0,prev:!0};function nt(t,e){for(;(t=t[e])&&1!==t.nodeType;);return t}x.fn.extend({has:function(t){var e=x(t,this),i=e.length;return this.filter(function(){for(var t=0;t<i;t++)if(x.contains(this,e[t]))return!0})},closest:function(t,e){var i,n=0,o=this.length,s=[],r="string"!=typeof t&&x(t);if(!Z.test(t))for(;n<o;n++)for(i=this[n];i&&i!==e;i=i.parentNode)if(i.nodeType<11&&(r?-1<r.index(i):1===i.nodeType&&x.find.matchesSelector(i,t))){s.push(i);break}return this.pushStack(1<s.length?x.uniqueSort(s):s)},index:function(t){return t?"string"==typeof t?R.call(x(t),this[0]):R.call(this,t.jquery?t[0]:t):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(t,e){return this.pushStack(x.uniqueSort(x.merge(this.get(),x(t,e))))},addBack:function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}}),x.each({parent:function(t){t=t.parentNode;return t&&11!==t.nodeType?t:null},parents:function(t){return n(t,"parentNode")},parentsUntil:function(t,e,i){return n(t,"parentNode",i)},next:function(t){return nt(t,"nextSibling")},prev:function(t){return nt(t,"previousSibling")},nextAll:function(t){return n(t,"nextSibling")},prevAll:function(t){return n(t,"previousSibling")},nextUntil:function(t,e,i){return n(t,"nextSibling",i)},prevUntil:function(t,e,i){return n(t,"previousSibling",i)},siblings:function(t){return X((t.parentNode||{}).firstChild,t)},children:function(t){return X(t.firstChild)},contents:function(t){return null!=t.contentDocument&&I(t.contentDocument)?t.contentDocument:(l(t,"template")&&(t=t.content||t),x.merge([],t.childNodes))}},function(n,o){x.fn[n]=function(t,e){var i=x.map(this,o,t);return(e="Until"!==n.slice(-5)?t:e)&&"string"==typeof e&&(i=x.filter(e,i)),1<this.length&&(it[n]||x.uniqueSort(i),et.test(n))&&i.reverse(),this.pushStack(i)}});var T=/[^\x20\t\r\n\f]+/g;function d(t){return t}function ot(t){throw t}function st(t,e,i,n){var o;try{t&&v(o=t.promise)?o.call(t).done(e).fail(i):t&&v(o=t.then)?o.call(t,e,i):e.apply(void 0,[t].slice(n))}catch(t){i.apply(void 0,[t])}}x.Callbacks=function(n){var t,i;n="string"==typeof n?(t=n,i={},x.each(t.match(T)||[],function(t,e){i[e]=!0}),i):x.extend({},n);function o(){for(a=a||n.once,r=s=!0;c.length;d=-1)for(e=c.shift();++d<l.length;)!1===l[d].apply(e[0],e[1])&&n.stopOnFalse&&(d=l.length,e=!1);n.memory||(e=!1),s=!1,a&&(l=e?[]:"")}var s,e,r,a,l=[],c=[],d=-1,u={add:function(){return l&&(e&&!s&&(d=l.length-1,c.push(e)),function i(t){x.each(t,function(t,e){v(e)?n.unique&&u.has(e)||l.push(e):e&&e.length&&"string"!==f(e)&&i(e)})}(arguments),e)&&!s&&o(),this},remove:function(){return x.each(arguments,function(t,e){for(var i;-1<(i=x.inArray(e,l,i));)l.splice(i,1),i<=d&&d--}),this},has:function(t){return t?-1<x.inArray(t,l):0<l.length},empty:function(){return l=l&&[],this},disable:function(){return a=c=[],l=e="",this},disabled:function(){return!l},lock:function(){return a=c=[],e||s||(l=e=""),this},locked:function(){return!!a},fireWith:function(t,e){return a||(e=[t,(e=e||[]).slice?e.slice():e],c.push(e),s)||o(),this},fire:function(){return u.fireWith(this,arguments),this},fired:function(){return!!r}};return u},x.extend({Deferred:function(t){var s=[["notify","progress",x.Callbacks("memory"),x.Callbacks("memory"),2],["resolve","done",x.Callbacks("once memory"),x.Callbacks("once memory"),0,"resolved"],["reject","fail",x.Callbacks("once memory"),x.Callbacks("once memory"),1,"rejected"]],o="pending",r={state:function(){return o},always:function(){return a.done(arguments).fail(arguments),this},catch:function(t){return r.then(null,t)},pipe:function(){var o=arguments;return x.Deferred(function(n){x.each(s,function(t,e){var i=v(o[e[4]])&&o[e[4]];a[e[1]](function(){var t=i&&i.apply(this,arguments);t&&v(t.promise)?t.promise().progress(n.notify).done(n.resolve).fail(n.reject):n[e[0]+"With"](this,i?[t]:arguments)})}),o=null}).promise()},then:function(e,i,n){var l=0;function c(o,s,r,a){return function(){function t(){var t,e;if(!(o<l)){if((t=r.apply(i,n))===s.promise())throw new TypeError("Thenable self-resolution");e=t&&("object"==typeof t||"function"==typeof t)&&t.then,v(e)?a?e.call(t,c(l,s,d,a),c(l,s,ot,a)):(l++,e.call(t,c(l,s,d,a),c(l,s,ot,a),c(l,s,d,s.notifyWith))):(r!==d&&(i=void 0,n=[t]),(a||s.resolveWith)(i,n))}}var i=this,n=arguments,e=a?t:function(){try{t()}catch(t){x.Deferred.exceptionHook&&x.Deferred.exceptionHook(t,e.stackTrace),l<=o+1&&(r!==ot&&(i=void 0,n=[t]),s.rejectWith(i,n))}};o?e():(x.Deferred.getStackHook&&(e.stackTrace=x.Deferred.getStackHook()),_.setTimeout(e))}}return x.Deferred(function(t){s[0][3].add(c(0,t,v(n)?n:d,t.notifyWith)),s[1][3].add(c(0,t,v(e)?e:d)),s[2][3].add(c(0,t,v(i)?i:ot))}).promise()},promise:function(t){return null!=t?x.extend(t,r):r}},a={};return x.each(s,function(t,e){var i=e[2],n=e[5];r[e[1]]=i.add,n&&i.add(function(){o=n},s[3-t][2].disable,s[3-t][3].disable,s[0][2].lock,s[0][3].lock),i.add(e[3].fire),a[e[0]]=function(){return a[e[0]+"With"](this===a?void 0:this,arguments),this},a[e[0]+"With"]=i.fireWith}),r.promise(a),t&&t.call(a,a),a},when:function(t){function e(e){return function(t){o[e]=this,s[e]=1<arguments.length?a.call(arguments):t,--i||r.resolveWith(o,s)}}var i=arguments.length,n=i,o=Array(n),s=a.call(arguments),r=x.Deferred();if(i<=1&&(st(t,r.done(e(n)).resolve,r.reject,!i),"pending"===r.state()||v(s[n]&&s[n].then)))return r.then();for(;n--;)st(s[n],e(n),r.reject);return r.promise()}});var rt=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/,at=(x.Deferred.exceptionHook=function(t,e){_.console&&_.console.warn&&t&&rt.test(t.name)&&_.console.warn("jQuery.Deferred exception: "+t.message,t.stack,e)},x.readyException=function(t){_.setTimeout(function(){throw t})},x.Deferred());function lt(){k.removeEventListener("DOMContentLoaded",lt),_.removeEventListener("load",lt),x.ready()}x.fn.ready=function(t){return at.then(t).catch(function(t){x.readyException(t)}),this},x.extend({isReady:!1,readyWait:1,ready:function(t){(!0===t?--x.readyWait:x.isReady)||(x.isReady=!0)!==t&&0<--x.readyWait||at.resolveWith(k,[x])}}),x.ready.then=at.then,"complete"===k.readyState||"loading"!==k.readyState&&!k.documentElement.doScroll?_.setTimeout(x.ready):(k.addEventListener("DOMContentLoaded",lt),_.addEventListener("load",lt));function u(t,e,i,n,o,s,r){var a=0,l=t.length,c=null==i;if("object"===f(i))for(a in o=!0,i)u(t,e,a,i[a],!0,s,r);else if(void 0!==n&&(o=!0,v(n)||(r=!0),e=c?r?(e.call(t,n),null):(c=e,function(t,e,i){return c.call(x(t),i)}):e))for(;a<l;a++)e(t[a],i,r?n:n.call(t[a],a,e(t[a],i)));return o?t:c?e.call(t):l?e(t[0],i):s}var ct=/^-ms-/,dt=/-([a-z])/g;function ut(t,e){return e.toUpperCase()}function b(t){return t.replace(ct,"ms-").replace(dt,ut)}function y(t){return 1===t.nodeType||9===t.nodeType||!+t.nodeType}function ht(){this.expando=x.expando+ht.uid++}ht.uid=1,ht.prototype={cache:function(t){var e=t[this.expando];return e||(e={},y(t)&&(t.nodeType?t[this.expando]=e:Object.defineProperty(t,this.expando,{value:e,configurable:!0}))),e},set:function(t,e,i){var n,o=this.cache(t);if("string"==typeof e)o[b(e)]=i;else for(n in e)o[b(n)]=e[n];return o},get:function(t,e){return void 0===e?this.cache(t):t[this.expando]&&t[this.expando][b(e)]},access:function(t,e,i){return void 0===e||e&&"string"==typeof e&&void 0===i?this.get(t,e):(this.set(t,e,i),void 0!==i?i:e)},remove:function(t,e){var i,n=t[this.expando];if(void 0!==n){if(void 0!==e){i=(e=Array.isArray(e)?e.map(b):(e=b(e))in n?[e]:e.match(T)||[]).length;for(;i--;)delete n[e[i]]}void 0!==e&&!x.isEmptyObject(n)||(t.nodeType?t[this.expando]=void 0:delete t[this.expando])}},hasData:function(t){t=t[this.expando];return void 0!==t&&!x.isEmptyObject(t)}};var w=new ht,c=new ht,pt=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,ft=/[A-Z]/g;function mt(t,e,i){var n,o;if(void 0===i&&1===t.nodeType)if(n="data-"+e.replace(ft,"-$&").toLowerCase(),"string"==typeof(i=t.getAttribute(n))){try{i="true"===(o=i)||"false"!==o&&("null"===o?null:o===+o+""?+o:pt.test(o)?JSON.parse(o):o)}catch(t){}c.set(t,e,i)}else i=void 0;return i}x.extend({hasData:function(t){return c.hasData(t)||w.hasData(t)},data:function(t,e,i){return c.access(t,e,i)},removeData:function(t,e){c.remove(t,e)},_data:function(t,e,i){return w.access(t,e,i)},_removeData:function(t,e){w.remove(t,e)}}),x.fn.extend({data:function(i,t){var e,n,o,s=this[0],r=s&&s.attributes;if(void 0!==i)return"object"==typeof i?this.each(function(){c.set(this,i)}):u(this,function(t){var e;if(s&&void 0===t)return void 0!==(e=c.get(s,i))||void 0!==(e=mt(s,i))?e:void 0;this.each(function(){c.set(this,i,t)})},null,t,1<arguments.length,null,!0);if(this.length&&(o=c.get(s),1===s.nodeType)&&!w.get(s,"hasDataAttrs")){for(e=r.length;e--;)r[e]&&0===(n=r[e].name).indexOf("data-")&&(n=b(n.slice(5)),mt(s,n,o[n]));w.set(s,"hasDataAttrs",!0)}return o},removeData:function(t){return this.each(function(){c.remove(this,t)})}}),x.extend({queue:function(t,e,i){var n;if(t)return n=w.get(t,e=(e||"fx")+"queue"),i&&(!n||Array.isArray(i)?n=w.access(t,e,x.makeArray(i)):n.push(i)),n||[]},dequeue:function(t,e){e=e||"fx";var i=x.queue(t,e),n=i.length,o=i.shift(),s=x._queueHooks(t,e);"inprogress"===o&&(o=i.shift(),n--),o&&("fx"===e&&i.unshift("inprogress"),delete s.stop,o.call(t,function(){x.dequeue(t,e)},s)),!n&&s&&s.empty.fire()},_queueHooks:function(t,e){var i=e+"queueHooks";return w.get(t,i)||w.access(t,i,{empty:x.Callbacks("once memory").add(function(){w.remove(t,[e+"queue",i])})})}}),x.fn.extend({queue:function(e,i){var t=2;return"string"!=typeof e&&(i=e,e="fx",t--),arguments.length<t?x.queue(this[0],e):void 0===i?this:this.each(function(){var t=x.queue(this,e,i);x._queueHooks(this,e),"fx"===e&&"inprogress"!==t[0]&&x.dequeue(this,e)})},dequeue:function(t){return this.each(function(){x.dequeue(this,t)})},clearQueue:function(t){return this.queue(t||"fx",[])},promise:function(t,e){function i(){--o||s.resolveWith(r,[r])}var n,o=1,s=x.Deferred(),r=this,a=this.length;for("string"!=typeof t&&(e=t,t=void 0),t=t||"fx";a--;)(n=w.get(r[a],t+"queueHooks"))&&n.empty&&(o++,n.empty.add(i));return i(),s.promise(e)}});function gt(t,e){return"none"===(t=e||t).style.display||""===t.style.display&&C(t)&&"none"===x.css(t,"display")}var t=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,yt=new RegExp("^(?:([+-])=|)("+t+")([a-z%]*)$","i"),h=["Top","Right","Bottom","Left"],S=k.documentElement,C=function(t){return x.contains(t.ownerDocument,t)},vt={composed:!0};S.getRootNode&&(C=function(t){return x.contains(t.ownerDocument,t)||t.getRootNode(vt)===t.ownerDocument});function bt(t,e,i,n){var o,s,r=20,a=n?function(){return n.cur()}:function(){return x.css(t,e,"")},l=a(),c=i&&i[3]||(x.cssNumber[e]?"":"px"),d=t.nodeType&&(x.cssNumber[e]||"px"!==c&&+l)&&yt.exec(x.css(t,e));if(d&&d[3]!==c){for(c=c||d[3],d=+(l/=2)||1;r--;)x.style(t,e,d+c),(1-s)*(1-(s=a()/l||.5))<=0&&(r=0),d/=s;x.style(t,e,(d*=2)+c),i=i||[]}return i&&(d=+d||+l||0,o=i[1]?d+(i[1]+1)*i[2]:+i[2],n)&&(n.unit=c,n.start=d,n.end=o),o}var wt={};function E(t,e){for(var i,n,o,s,r,a=[],l=0,c=t.length;l<c;l++)(n=t[l]).style&&(i=n.style.display,e?("none"===i&&(a[l]=w.get(n,"display")||null,a[l]||(n.style.display="")),""===n.style.display&>(n)&&(a[l]=(r=s=void 0,s=(o=n).ownerDocument,o=o.nodeName,(r=wt[o])||(s=s.body.appendChild(s.createElement(o)),r=x.css(s,"display"),s.parentNode.removeChild(s),wt[o]=r="none"===r?"block":r),r))):"none"!==i&&(a[l]="none",w.set(n,"display",i)));for(l=0;l<c;l++)null!=a[l]&&(t[l].style.display=a[l]);return t}x.fn.extend({show:function(){return E(this,!0)},hide:function(){return E(this)},toggle:function(t){return"boolean"==typeof t?t?this.show():this.hide():this.each(function(){gt(this)?x(this).show():x(this).hide()})}});var _t=/^(?:checkbox|radio)$/i,kt=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i,xt=/^$|^module$|\/(?:java|ecma)script/i,D=($=k.createDocumentFragment().appendChild(k.createElement("div")),(s=k.createElement("input")).setAttribute("type","radio"),s.setAttribute("checked","checked"),s.setAttribute("name","t"),$.appendChild(s),g.checkClone=$.cloneNode(!0).cloneNode(!0).lastChild.checked,$.innerHTML="<textarea>x</textarea>",g.noCloneChecked=!!$.cloneNode(!0).lastChild.defaultValue,$.innerHTML="<option></option>",g.option=!!$.lastChild,{thead:[1,"<table>","</table>"],col:[2,"<table><colgroup>","</colgroup></table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:[0,"",""]});function A(t,e){var i=void 0!==t.getElementsByTagName?t.getElementsByTagName(e||"*"):void 0!==t.querySelectorAll?t.querySelectorAll(e||"*"):[];return void 0===e||e&&l(t,e)?x.merge([t],i):i}function Tt(t,e){for(var i=0,n=t.length;i<n;i++)w.set(t[i],"globalEval",!e||w.get(e[i],"globalEval"))}D.tbody=D.tfoot=D.colgroup=D.caption=D.thead,D.th=D.td,g.option||(D.optgroup=D.option=[1,"<select multiple='multiple'>","</select>"]);var St=/<|&#?\w+;/;function Ct(t,e,i,n,o){for(var s,r,a,l,c,d=e.createDocumentFragment(),u=[],h=0,p=t.length;h<p;h++)if((s=t[h])||0===s)if("object"===f(s))x.merge(u,s.nodeType?[s]:s);else if(St.test(s)){for(r=r||d.appendChild(e.createElement("div")),a=(kt.exec(s)||["",""])[1].toLowerCase(),a=D[a]||D._default,r.innerHTML=a[1]+x.htmlPrefilter(s)+a[2],c=a[0];c--;)r=r.lastChild;x.merge(u,r.childNodes),(r=d.firstChild).textContent=""}else u.push(e.createTextNode(s));for(d.textContent="",h=0;s=u[h++];)if(n&&-1<x.inArray(s,n))o&&o.push(s);else if(l=C(s),r=A(d.appendChild(s),"script"),l&&Tt(r),i)for(c=0;s=r[c++];)xt.test(s.type||"")&&i.push(s);return d}var Et=/^([^.]*)(?:\.(.+)|)/;function i(){return!0}function p(){return!1}function Dt(t,e){return t===function(){try{return k.activeElement}catch(t){}}()==("focus"===e)}function At(t,e,i,n,o,s){var r,a;if("object"==typeof e){for(a in"string"!=typeof i&&(n=n||i,i=void 0),e)At(t,a,i,n,e[a],s);return t}if(null==n&&null==o?(o=i,n=i=void 0):null==o&&("string"==typeof i?(o=n,n=void 0):(o=n,n=i,i=void 0)),!1===o)o=p;else if(!o)return t;return 1===s&&(r=o,(o=function(t){return x().off(t),r.apply(this,arguments)}).guid=r.guid||(r.guid=x.guid++)),t.each(function(){x.event.add(this,e,o,n,i)})}function Lt(t,o,s){s?(w.set(t,o,!1),x.event.add(t,o,{namespace:!1,handler:function(t){var e,i,n=w.get(this,o);if(1&t.isTrigger&&this[o]){if(n.length)(x.event.special[o]||{}).delegateType&&t.stopPropagation();else if(n=a.call(arguments),w.set(this,o,n),e=s(this,o),this[o](),n!==(i=w.get(this,o))||e?w.set(this,o,!1):i={},n!==i)return t.stopImmediatePropagation(),t.preventDefault(),i&&i.value}else n.length&&(w.set(this,o,{value:x.event.trigger(x.extend(n[0],x.Event.prototype),n.slice(1),this)}),t.stopImmediatePropagation())}})):void 0===w.get(t,o)&&x.event.add(t,o,i)}x.event={global:{},add:function(e,t,i,n,o){var s,r,a,l,c,d,u,h,p,f=w.get(e);if(y(e))for(i.handler&&(i=(s=i).handler,o=s.selector),o&&x.find.matchesSelector(S,o),i.guid||(i.guid=x.guid++),a=(a=f.events)||(f.events=Object.create(null)),r=(r=f.handle)||(f.handle=function(t){return void 0!==x&&x.event.triggered!==t.type?x.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(T)||[""]).length;l--;)u=p=(h=Et.exec(t[l])||[])[1],h=(h[2]||"").split(".").sort(),u&&(c=x.event.special[u]||{},u=(o?c.delegateType:c.bindType)||u,c=x.event.special[u]||{},p=x.extend({type:u,origType:p,data:n,handler:i,guid:i.guid,selector:o,needsContext:o&&x.expr.match.needsContext.test(o),namespace:h.join(".")},s),(d=a[u])||((d=a[u]=[]).delegateCount=0,c.setup&&!1!==c.setup.call(e,n,h,r))||e.addEventListener&&e.addEventListener(u,r),c.add&&(c.add.call(e,p),p.handler.guid||(p.handler.guid=i.guid)),o?d.splice(d.delegateCount++,0,p):d.push(p),x.event.global[u]=!0)},remove:function(t,e,i,n,o){var s,r,a,l,c,d,u,h,p,f,m,g=w.hasData(t)&&w.get(t);if(g&&(l=g.events)){for(c=(e=(e||"").match(T)||[""]).length;c--;)if(p=m=(a=Et.exec(e[c])||[])[1],f=(a[2]||"").split(".").sort(),p){for(u=x.event.special[p]||{},h=l[p=(n?u.delegateType:u.bindType)||p]||[],a=a[2]&&new RegExp("(^|\\.)"+f.join("\\.(?:.*\\.|)")+"(\\.|$)"),r=s=h.length;s--;)d=h[s],!o&&m!==d.origType||i&&i.guid!==d.guid||a&&!a.test(d.namespace)||n&&n!==d.selector&&("**"!==n||!d.selector)||(h.splice(s,1),d.selector&&h.delegateCount--,u.remove&&u.remove.call(t,d));r&&!h.length&&(u.teardown&&!1!==u.teardown.call(t,f,g.handle)||x.removeEvent(t,p,g.handle),delete l[p])}else for(p in l)x.event.remove(t,p+e[c],i,n,!0);x.isEmptyObject(l)&&w.remove(t,"handle events")}},dispatch:function(t){var e,i,n,o,s,r=new Array(arguments.length),a=x.event.fix(t),t=(w.get(this,"events")||Object.create(null))[a.type]||[],l=x.event.special[a.type]||{};for(r[0]=a,e=1;e<arguments.length;e++)r[e]=arguments[e];if(a.delegateTarget=this,!l.preDispatch||!1!==l.preDispatch.call(this,a)){for(s=x.event.handlers.call(this,a,t),e=0;(n=s[e++])&&!a.isPropagationStopped();)for(a.currentTarget=n.elem,i=0;(o=n.handlers[i++])&&!a.isImmediatePropagationStopped();)a.rnamespace&&!1!==o.namespace&&!a.rnamespace.test(o.namespace)||(a.handleObj=o,a.data=o.data,void 0!==(o=((x.event.special[o.origType]||{}).handle||o.handler).apply(n.elem,r))&&!1===(a.result=o)&&(a.preventDefault(),a.stopPropagation()));return l.postDispatch&&l.postDispatch.call(this,a),a.result}},handlers:function(t,e){var i,n,o,s,r,a=[],l=e.delegateCount,c=t.target;if(l&&c.nodeType&&!("click"===t.type&&1<=t.button))for(;c!==this;c=c.parentNode||this)if(1===c.nodeType&&("click"!==t.type||!0!==c.disabled)){for(s=[],r={},i=0;i<l;i++)void 0===r[o=(n=e[i]).selector+" "]&&(r[o]=n.needsContext?-1<x(o,this).index(c):x.find(o,this,null,[c]).length),r[o]&&s.push(n);s.length&&a.push({elem:c,handlers:s})}return c=this,l<e.length&&a.push({elem:c,handlers:e.slice(l)}),a},addProp:function(e,t){Object.defineProperty(x.Event.prototype,e,{enumerable:!0,configurable:!0,get:v(t)?function(){if(this.originalEvent)return t(this.originalEvent)}:function(){if(this.originalEvent)return this.originalEvent[e]},set:function(t){Object.defineProperty(this,e,{enumerable:!0,configurable:!0,writable:!0,value:t})}})},fix:function(t){return t[x.expando]?t:new x.Event(t)},special:{load:{noBubble:!0},click:{setup:function(t){t=this||t;return _t.test(t.type)&&t.click&&l(t,"input")&&Lt(t,"click",i),!1},trigger:function(t){t=this||t;return _t.test(t.type)&&t.click&&l(t,"input")&&Lt(t,"click"),!0},_default:function(t){t=t.target;return _t.test(t.type)&&t.click&&l(t,"input")&&w.get(t,"click")||l(t,"a")}},beforeunload:{postDispatch:function(t){void 0!==t.result&&t.originalEvent&&(t.originalEvent.returnValue=t.result)}}}},x.removeEvent=function(t,e,i){t.removeEventListener&&t.removeEventListener(e,i)},x.Event=function(t,e){if(!(this instanceof x.Event))return new x.Event(t,e);t&&t.type?(this.originalEvent=t,this.type=t.type,this.isDefaultPrevented=t.defaultPrevented||void 0===t.defaultPrevented&&!1===t.returnValue?i:p,this.target=t.target&&3===t.target.nodeType?t.target.parentNode:t.target,this.currentTarget=t.currentTarget,this.relatedTarget=t.relatedTarget):this.type=t,e&&x.extend(this,e),this.timeStamp=t&&t.timeStamp||Date.now(),this[x.expando]=!0},x.Event.prototype={constructor:x.Event,isDefaultPrevented:p,isPropagationStopped:p,isImmediatePropagationStopped:p,isSimulated:!1,preventDefault:function(){var t=this.originalEvent;this.isDefaultPrevented=i,t&&!this.isSimulated&&t.preventDefault()},stopPropagation:function(){var t=this.originalEvent;this.isPropagationStopped=i,t&&!this.isSimulated&&t.stopPropagation()},stopImmediatePropagation:function(){var t=this.originalEvent;this.isImmediatePropagationStopped=i,t&&!this.isSimulated&&t.stopImmediatePropagation(),this.stopPropagation()}},x.each({altKey:!0,bubbles:!0,cancelable:!0,changedTouches:!0,ctrlKey:!0,detail:!0,eventPhase:!0,metaKey:!0,pageX:!0,pageY:!0,shiftKey:!0,view:!0,char:!0,code:!0,charCode:!0,key:!0,keyCode:!0,button:!0,buttons:!0,clientX:!0,clientY:!0,offsetX:!0,offsetY:!0,pointerId:!0,pointerType:!0,screenX:!0,screenY:!0,targetTouches:!0,toElement:!0,touches:!0,which:!0},x.event.addProp),x.each({focus:"focusin",blur:"focusout"},function(e,t){x.event.special[e]={setup:function(){return Lt(this,e,Dt),!1},trigger:function(){return Lt(this,e),!0},_default:function(t){return w.get(t.target,e)},delegateType:t}}),x.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(t,o){x.event.special[t]={delegateType:o,bindType:o,handle:function(t){var e,i=t.relatedTarget,n=t.handleObj;return i&&(i===this||x.contains(this,i))||(t.type=n.origType,e=n.handler.apply(this,arguments),t.type=o),e}}}),x.fn.extend({on:function(t,e,i,n){return At(this,t,e,i,n)},one:function(t,e,i,n){return At(this,t,e,i,n,1)},off:function(t,e,i){var n,o;if(t&&t.preventDefault&&t.handleObj)n=t.handleObj,x(t.delegateTarget).off(n.namespace?n.origType+"."+n.namespace:n.origType,n.selector,n.handler);else{if("object"!=typeof t)return!1!==e&&"function"!=typeof e||(i=e,e=void 0),!1===i&&(i=p),this.each(function(){x.event.remove(this,t,i,e)});for(o in t)this.off(o,e,t[o])}return this}});var Ot=/<script|<style|<link/i,$t=/checked\s*(?:[^=]|=\s*.checked.)/i,Mt=/^\s*<!\[CDATA\[|\]\]>\s*$/g;function jt(t,e){return l(t,"table")&&l(11!==e.nodeType?e:e.firstChild,"tr")&&x(t).children("tbody")[0]||t}function Pt(t){return t.type=(null!==t.getAttribute("type"))+"/"+t.type,t}function Nt(t){return"true/"===(t.type||"").slice(0,5)?t.type=t.type.slice(5):t.removeAttribute("type"),t}function It(t,e){var i,n,o,s;if(1===e.nodeType){if(w.hasData(t)&&(s=w.get(t).events))for(o in w.remove(e,"handle events"),s)for(i=0,n=s[o].length;i<n;i++)x.event.add(e,o,s[o][i]);c.hasData(t)&&(t=c.access(t),t=x.extend({},t),c.set(e,t))}}function L(i,n,o,s){n=H(n);var t,e,r,a,l,c,d=0,u=i.length,h=u-1,p=n[0],f=v(p);if(f||1<u&&"string"==typeof p&&!g.checkClone&&$t.test(p))return i.each(function(t){var e=i.eq(t);f&&(n[0]=p.call(this,t,e.html())),L(e,n,o,s)});if(u&&(e=(t=Ct(n,i[0].ownerDocument,!1,i,s)).firstChild,1===t.childNodes.length&&(t=e),e||s)){for(a=(r=x.map(A(t,"script"),Pt)).length;d<u;d++)l=t,d!==h&&(l=x.clone(l,!0,!0),a)&&x.merge(r,A(l,"script")),o.call(i[d],l,d);if(a)for(c=r[r.length-1].ownerDocument,x.map(r,Nt),d=0;d<a;d++)l=r[d],xt.test(l.type||"")&&!w.access(l,"globalEval")&&x.contains(c,l)&&(l.src&&"module"!==(l.type||"").toLowerCase()?x._evalUrl&&!l.noModule&&x._evalUrl(l.src,{nonce:l.nonce||l.getAttribute("nonce")},c):G(l.textContent.replace(Mt,""),l,c))}return i}function Ht(t,e,i){for(var n,o=e?x.filter(e,t):t,s=0;null!=(n=o[s]);s++)i||1!==n.nodeType||x.cleanData(A(n)),n.parentNode&&(i&&C(n)&&Tt(A(n,"script")),n.parentNode.removeChild(n));return t}x.extend({htmlPrefilter:function(t){return t},clone:function(t,e,i){var n,o,s,r,a,l,c,d=t.cloneNode(!0),u=C(t);if(!(g.noCloneChecked||1!==t.nodeType&&11!==t.nodeType||x.isXMLDoc(t)))for(r=A(d),n=0,o=(s=A(t)).length;n<o;n++)a=s[n],l=r[n],c=void 0,"input"===(c=l.nodeName.toLowerCase())&&_t.test(a.type)?l.checked=a.checked:"input"!==c&&"textarea"!==c||(l.defaultValue=a.defaultValue);if(e)if(i)for(s=s||A(t),r=r||A(d),n=0,o=s.length;n<o;n++)It(s[n],r[n]);else It(t,d);return 0<(r=A(d,"script")).length&&Tt(r,!u&&A(t,"script")),d},cleanData:function(t){for(var e,i,n,o=x.event.special,s=0;void 0!==(i=t[s]);s++)if(y(i)){if(e=i[w.expando]){if(e.events)for(n in e.events)o[n]?x.event.remove(i,n):x.removeEvent(i,n,e.handle);i[w.expando]=void 0}i[c.expando]&&(i[c.expando]=void 0)}}}),x.fn.extend({detach:function(t){return Ht(this,t,!0)},remove:function(t){return Ht(this,t)},text:function(t){return u(this,function(t){return void 0===t?x.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=t)})},null,t,arguments.length)},append:function(){return L(this,arguments,function(t){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||jt(this,t).appendChild(t)})},prepend:function(){return L(this,arguments,function(t){var e;1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(e=jt(this,t)).insertBefore(t,e.firstChild)})},before:function(){return L(this,arguments,function(t){this.parentNode&&this.parentNode.insertBefore(t,this)})},after:function(){return L(this,arguments,function(t){this.parentNode&&this.parentNode.insertBefore(t,this.nextSibling)})},empty:function(){for(var t,e=0;null!=(t=this[e]);e++)1===t.nodeType&&(x.cleanData(A(t,!1)),t.textContent="");return this},clone:function(t,e){return t=null!=t&&t,e=null==e?t:e,this.map(function(){return x.clone(this,t,e)})},html:function(t){return u(this,function(t){var e=this[0]||{},i=0,n=this.length;if(void 0===t&&1===e.nodeType)return e.innerHTML;if("string"==typeof t&&!Ot.test(t)&&!D[(kt.exec(t)||["",""])[1].toLowerCase()]){t=x.htmlPrefilter(t);try{for(;i<n;i++)1===(e=this[i]||{}).nodeType&&(x.cleanData(A(e,!1)),e.innerHTML=t);e=0}catch(t){}}e&&this.empty().append(t)},null,t,arguments.length)},replaceWith:function(){var i=[];return L(this,arguments,function(t){var e=this.parentNode;x.inArray(this,i)<0&&(x.cleanData(A(this)),e)&&e.replaceChild(t,this)},i)}}),x.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(t,r){x.fn[t]=function(t){for(var e,i=[],n=x(t),o=n.length-1,s=0;s<=o;s++)e=s===o?this:this.clone(!0),x(n[s])[r](e),F.apply(i,e.get());return this.pushStack(i)}});function Ft(t){var e=t.ownerDocument.defaultView;return(e=e&&e.opener?e:_).getComputedStyle(t)}function Rt(t,e,i){var n,o={};for(n in e)o[n]=t.style[n],t.style[n]=e[n];for(n in i=i.call(t),e)t.style[n]=o[n];return i}var qt,Yt,Wt,Bt,zt,Ut,Gt,o,Vt=new RegExp("^("+t+")(?!px)[a-z%]+$","i"),Xt=/^--/,Zt=new RegExp(h.join("|"),"i"),s="[\\x20\\t\\r\\n\\f]",Qt=new RegExp("^"+s+"+|((?:^|[^\\\\])(?:\\\\.)*)"+s+"+$","g");function Jt(){var t;o&&(Gt.style.cssText="position:absolute;left:-11111px;width:60px;margin-top:1px;padding:0;border:0",o.style.cssText="position:relative;display:block;box-sizing:border-box;overflow:scroll;margin:auto;border:1px;padding:1px;width:60%;top:1%",S.appendChild(Gt).appendChild(o),t=_.getComputedStyle(o),qt="1%"!==t.top,Ut=12===Kt(t.marginLeft),o.style.right="60%",Bt=36===Kt(t.right),Yt=36===Kt(t.width),o.style.position="absolute",Wt=12===Kt(o.offsetWidth/3),S.removeChild(Gt),o=null)}function Kt(t){return Math.round(parseFloat(t))}function te(t,e,i){var n,o=Xt.test(e),s=t.style;return(i=i||Ft(t))&&(n=i.getPropertyValue(e)||i[e],""!==(n=o?n&&(n.replace(Qt,"$1")||void 0):n)||C(t)||(n=x.style(t,e)),!g.pixelBoxStyles())&&Vt.test(n)&&Zt.test(e)&&(o=s.width,t=s.minWidth,e=s.maxWidth,s.minWidth=s.maxWidth=s.width=n,n=i.width,s.width=o,s.minWidth=t,s.maxWidth=e),void 0!==n?n+"":n}function ee(t,e){return{get:function(){if(!t())return(this.get=e).apply(this,arguments);delete this.get}}}Gt=k.createElement("div"),(o=k.createElement("div")).style&&(o.style.backgroundClip="content-box",o.cloneNode(!0).style.backgroundClip="",g.clearCloneStyle="content-box"===o.style.backgroundClip,x.extend(g,{boxSizingReliable:function(){return Jt(),Yt},pixelBoxStyles:function(){return Jt(),Bt},pixelPosition:function(){return Jt(),qt},reliableMarginLeft:function(){return Jt(),Ut},scrollboxSize:function(){return Jt(),Wt},reliableTrDimensions:function(){var t,e,i;return null==zt&&(t=k.createElement("table"),e=k.createElement("tr"),i=k.createElement("div"),t.style.cssText="position:absolute;left:-11111px;border-collapse:separate",e.style.cssText="border:1px solid",e.style.height="1px",i.style.height="9px",i.style.display="block",S.appendChild(t).appendChild(e).appendChild(i),i=_.getComputedStyle(e),zt=parseInt(i.height,10)+parseInt(i.borderTopWidth,10)+parseInt(i.borderBottomWidth,10)===e.offsetHeight,S.removeChild(t)),zt}}));var ie=["Webkit","Moz","ms"],ne=k.createElement("div").style,oe={};function se(t){var e=x.cssProps[t]||oe[t];return e||(t in ne?t:oe[t]=function(t){for(var e=t[0].toUpperCase()+t.slice(1),i=ie.length;i--;)if((t=ie[i]+e)in ne)return t}(t)||t)}var re=/^(none|table(?!-c[ea]).+)/,ae={position:"absolute",visibility:"hidden",display:"block"},le={letterSpacing:"0",fontWeight:"400"};function ce(t,e,i){var n=yt.exec(e);return n?Math.max(0,n[2]-(i||0))+(n[3]||"px"):e}function de(t,e,i,n,o,s){var r="width"===e?1:0,a=0,l=0;if(i===(n?"border":"content"))return 0;for(;r<4;r+=2)"margin"===i&&(l+=x.css(t,i+h[r],!0,o)),n?("content"===i&&(l-=x.css(t,"padding"+h[r],!0,o)),"margin"!==i&&(l-=x.css(t,"border"+h[r]+"Width",!0,o))):(l+=x.css(t,"padding"+h[r],!0,o),"padding"!==i?l+=x.css(t,"border"+h[r]+"Width",!0,o):a+=x.css(t,"border"+h[r]+"Width",!0,o));return!n&&0<=s&&(l+=Math.max(0,Math.ceil(t["offset"+e[0].toUpperCase()+e.slice(1)]-s-l-a-.5))||0),l}function ue(t,e,i){var n=Ft(t),o=(!g.boxSizingReliable()||i)&&"border-box"===x.css(t,"boxSizing",!1,n),s=o,r=te(t,e,n),a="offset"+e[0].toUpperCase()+e.slice(1);if(Vt.test(r)){if(!i)return r;r="auto"}return(!g.boxSizingReliable()&&o||!g.reliableTrDimensions()&&l(t,"tr")||"auto"===r||!parseFloat(r)&&"inline"===x.css(t,"display",!1,n))&&t.getClientRects().length&&(o="border-box"===x.css(t,"boxSizing",!1,n),s=a in t)&&(r=t[a]),(r=parseFloat(r)||0)+de(t,e,i||(o?"border":"content"),s,n,r)+"px"}function r(t,e,i,n,o){return new r.prototype.init(t,e,i,n,o)}x.extend({cssHooks:{opacity:{get:function(t,e){if(e)return""===(e=te(t,"opacity"))?"1":e}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(t,e,i,n){if(t&&3!==t.nodeType&&8!==t.nodeType&&t.style){var o,s,r,a=b(e),l=Xt.test(e),c=t.style;if(l||(e=se(a)),r=x.cssHooks[e]||x.cssHooks[a],void 0===i)return r&&"get"in r&&void 0!==(o=r.get(t,!1,n))?o:c[e];"string"===(s=typeof i)&&(o=yt.exec(i))&&o[1]&&(i=bt(t,e,o),s="number"),null==i||i!=i||("number"!==s||l||(i+=o&&o[3]||(x.cssNumber[a]?"":"px")),g.clearCloneStyle||""!==i||0!==e.indexOf("background")||(c[e]="inherit"),r&&"set"in r&&void 0===(i=r.set(t,i,n)))||(l?c.setProperty(e,i):c[e]=i)}},css:function(t,e,i,n){var o,s=b(e);return Xt.test(e)||(e=se(s)),"normal"===(o=void 0===(o=(s=x.cssHooks[e]||x.cssHooks[s])&&"get"in s?s.get(t,!0,i):o)?te(t,e,n):o)&&e in le&&(o=le[e]),(""===i||i)&&(s=parseFloat(o),!0===i||isFinite(s))?s||0:o}}),x.each(["height","width"],function(t,r){x.cssHooks[r]={get:function(t,e,i){if(e)return!re.test(x.css(t,"display"))||t.getClientRects().length&&t.getBoundingClientRect().width?ue(t,r,i):Rt(t,ae,function(){return ue(t,r,i)})},set:function(t,e,i){var n=Ft(t),o=!g.scrollboxSize()&&"absolute"===n.position,s=(o||i)&&"border-box"===x.css(t,"boxSizing",!1,n),i=i?de(t,r,i,s,n):0;return s&&o&&(i-=Math.ceil(t["offset"+r[0].toUpperCase()+r.slice(1)]-parseFloat(n[r])-de(t,r,"border",!1,n)-.5)),i&&(s=yt.exec(e))&&"px"!==(s[3]||"px")&&(t.style[r]=e,e=x.css(t,r)),ce(0,e,i)}}}),x.cssHooks.marginLeft=ee(g.reliableMarginLeft,function(t,e){if(e)return(parseFloat(te(t,"marginLeft"))||t.getBoundingClientRect().left-Rt(t,{marginLeft:0},function(){return t.getBoundingClientRect().left}))+"px"}),x.each({margin:"",padding:"",border:"Width"},function(o,s){x.cssHooks[o+s]={expand:function(t){for(var e=0,i={},n="string"==typeof t?t.split(" "):[t];e<4;e++)i[o+h[e]+s]=n[e]||n[e-2]||n[0];return i}},"margin"!==o&&(x.cssHooks[o+s].set=ce)}),x.fn.extend({css:function(t,e){return u(this,function(t,e,i){var n,o,s={},r=0;if(Array.isArray(e)){for(n=Ft(t),o=e.length;r<o;r++)s[e[r]]=x.css(t,e[r],!1,n);return s}return void 0!==i?x.style(t,e,i):x.css(t,e)},t,e,1<arguments.length)}}),((x.Tween=r).prototype={constructor:r,init:function(t,e,i,n,o,s){this.elem=t,this.prop=i,this.easing=o||x.easing._default,this.options=e,this.start=this.now=this.cur(),this.end=n,this.unit=s||(x.cssNumber[i]?"":"px")},cur:function(){var t=r.propHooks[this.prop];return(t&&t.get?t:r.propHooks._default).get(this)},run:function(t){var e,i=r.propHooks[this.prop];return this.options.duration?this.pos=e=x.easing[this.easing](t,this.options.duration*t,0,1,this.options.duration):this.pos=e=t,this.now=(this.end-this.start)*e+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),(i&&i.set?i:r.propHooks._default).set(this),this}}).init.prototype=r.prototype,(r.propHooks={_default:{get:function(t){return 1!==t.elem.nodeType||null!=t.elem[t.prop]&&null==t.elem.style[t.prop]?t.elem[t.prop]:(t=x.css(t.elem,t.prop,""))&&"auto"!==t?t:0},set:function(t){x.fx.step[t.prop]?x.fx.step[t.prop](t):1!==t.elem.nodeType||!x.cssHooks[t.prop]&&null==t.elem.style[se(t.prop)]?t.elem[t.prop]=t.now:x.style(t.elem,t.prop,t.now+t.unit)}}}).scrollTop=r.propHooks.scrollLeft={set:function(t){t.elem.nodeType&&t.elem.parentNode&&(t.elem[t.prop]=t.now)}},x.easing={linear:function(t){return t},swing:function(t){return.5-Math.cos(t*Math.PI)/2},_default:"swing"},x.fx=r.prototype.init,x.fx.step={};var O,he,$,pe=/^(?:toggle|show|hide)$/,fe=/queueHooks$/;function me(){he&&(!1===k.hidden&&_.requestAnimationFrame?_.requestAnimationFrame(me):_.setTimeout(me,x.fx.interval),x.fx.tick())}function ge(){return _.setTimeout(function(){O=void 0}),O=Date.now()}function ye(t,e){var i,n=0,o={height:t};for(e=e?1:0;n<4;n+=2-e)o["margin"+(i=h[n])]=o["padding"+i]=t;return e&&(o.opacity=o.width=t),o}function ve(t,e,i){for(var n,o=(M.tweeners[e]||[]).concat(M.tweeners["*"]),s=0,r=o.length;s<r;s++)if(n=o[s].call(i,e,t))return n}function M(o,t,e){var i,s,n,r,a,l,c,d=0,u=M.prefilters.length,h=x.Deferred().always(function(){delete p.elem}),p=function(){if(!s){for(var t=O||ge(),t=Math.max(0,f.startTime+f.duration-t),e=1-(t/f.duration||0),i=0,n=f.tweens.length;i<n;i++)f.tweens[i].run(e);if(h.notifyWith(o,[f,e,t]),e<1&&n)return t;n||h.notifyWith(o,[f,1,0]),h.resolveWith(o,[f])}return!1},f=h.promise({elem:o,props:x.extend({},t),opts:x.extend(!0,{specialEasing:{},easing:x.easing._default},e),originalProperties:t,originalOptions:e,startTime:O||ge(),duration:e.duration,tweens:[],createTween:function(t,e){e=x.Tween(o,f.opts,t,e,f.opts.specialEasing[t]||f.opts.easing);return f.tweens.push(e),e},stop:function(t){var e=0,i=t?f.tweens.length:0;if(!s){for(s=!0;e<i;e++)f.tweens[e].run(1);t?(h.notifyWith(o,[f,1,0]),h.resolveWith(o,[f,t])):h.rejectWith(o,[f,t])}return this}}),m=f.props,g=m,y=f.opts.specialEasing;for(n in g)if(a=y[r=b(n)],l=g[n],Array.isArray(l)&&(a=l[1],l=g[n]=l[0]),n!==r&&(g[r]=l,delete g[n]),(c=x.cssHooks[r])&&"expand"in c)for(n in l=c.expand(l),delete g[r],l)n in g||(g[n]=l[n],y[n]=a);else y[r]=a;for(;d<u;d++)if(i=M.prefilters[d].call(f,o,m,f.opts))return v(i.stop)&&(x._queueHooks(f.elem,f.opts.queue).stop=i.stop.bind(i)),i;return x.map(m,ve,f),v(f.opts.start)&&f.opts.start.call(o,f),f.progress(f.opts.progress).done(f.opts.done,f.opts.complete).fail(f.opts.fail).always(f.opts.always),x.fx.timer(x.extend(p,{elem:o,anim:f,queue:f.opts.queue})),f}x.Animation=x.extend(M,{tweeners:{"*":[function(t,e){var i=this.createTween(t,e);return bt(i.elem,t,yt.exec(e),i),i}]},tweener:function(t,e){for(var i,n=0,o=(t=v(t)?(e=t,["*"]):t.match(T)).length;n<o;n++)i=t[n],M.tweeners[i]=M.tweeners[i]||[],M.tweeners[i].unshift(e)},prefilters:[function(t,e,i){var n,o,s,r,a,l,c,d="width"in e||"height"in e,u=this,h={},p=t.style,f=t.nodeType&>(t),m=w.get(t,"fxshow");for(n in i.queue||(null==(r=x._queueHooks(t,"fx")).unqueued&&(r.unqueued=0,a=r.empty.fire,r.empty.fire=function(){r.unqueued||a()}),r.unqueued++,u.always(function(){u.always(function(){r.unqueued--,x.queue(t,"fx").length||r.empty.fire()})})),e)if(o=e[n],pe.test(o)){if(delete e[n],s=s||"toggle"===o,o===(f?"hide":"show")){if("show"!==o||!m||void 0===m[n])continue;f=!0}h[n]=m&&m[n]||x.style(t,n)}if((l=!x.isEmptyObject(e))||!x.isEmptyObject(h))for(n in d&&1===t.nodeType&&(i.overflow=[p.overflow,p.overflowX,p.overflowY],null==(c=m&&m.display)&&(c=w.get(t,"display")),"none"===(d=x.css(t,"display"))&&(c?d=c:(E([t],!0),c=t.style.display||c,d=x.css(t,"display"),E([t]))),"inline"===d||"inline-block"===d&&null!=c)&&"none"===x.css(t,"float")&&(l||(u.done(function(){p.display=c}),null==c&&(d=p.display,c="none"===d?"":d)),p.display="inline-block"),i.overflow&&(p.overflow="hidden",u.always(function(){p.overflow=i.overflow[0],p.overflowX=i.overflow[1],p.overflowY=i.overflow[2]})),l=!1,h)l||(m?"hidden"in m&&(f=m.hidden):m=w.access(t,"fxshow",{display:c}),s&&(m.hidden=!f),f&&E([t],!0),u.done(function(){for(n in f||E([t]),w.remove(t,"fxshow"),h)x.style(t,n,h[n])})),l=ve(f?m[n]:0,n,u),n in m||(m[n]=l.start,f&&(l.end=l.start,l.start=0))}],prefilter:function(t,e){e?M.prefilters.unshift(t):M.prefilters.push(t)}}),x.speed=function(t,e,i){var n=t&&"object"==typeof t?x.extend({},t):{complete:i||!i&&e||v(t)&&t,duration:t,easing:i&&e||e&&!v(e)&&e};return x.fx.off?n.duration=0:"number"!=typeof n.duration&&(n.duration in x.fx.speeds?n.duration=x.fx.speeds[n.duration]:n.duration=x.fx.speeds._default),null!=n.queue&&!0!==n.queue||(n.queue="fx"),n.old=n.complete,n.complete=function(){v(n.old)&&n.old.call(this),n.queue&&x.dequeue(this,n.queue)},n},x.fn.extend({fadeTo:function(t,e,i,n){return this.filter(gt).css("opacity",0).show().end().animate({opacity:e},t,i,n)},animate:function(e,t,i,n){function o(){var t=M(this,x.extend({},e),r);(s||w.get(this,"finish"))&&t.stop(!0)}var s=x.isEmptyObject(e),r=x.speed(t,i,n);return o.finish=o,s||!1===r.queue?this.each(o):this.queue(r.queue,o)},stop:function(o,t,s){function r(t){var e=t.stop;delete t.stop,e(s)}return"string"!=typeof o&&(s=t,t=o,o=void 0),t&&this.queue(o||"fx",[]),this.each(function(){var t=!0,e=null!=o&&o+"queueHooks",i=x.timers,n=w.get(this);if(e)n[e]&&n[e].stop&&r(n[e]);else for(e in n)n[e]&&n[e].stop&&fe.test(e)&&r(n[e]);for(e=i.length;e--;)i[e].elem!==this||null!=o&&i[e].queue!==o||(i[e].anim.stop(s),t=!1,i.splice(e,1));!t&&s||x.dequeue(this,o)})},finish:function(r){return!1!==r&&(r=r||"fx"),this.each(function(){var t,e=w.get(this),i=e[r+"queue"],n=e[r+"queueHooks"],o=x.timers,s=i?i.length:0;for(e.finish=!0,x.queue(this,r,[]),n&&n.stop&&n.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===r&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;t<s;t++)i[t]&&i[t].finish&&i[t].finish.call(this);delete e.finish})}}),x.each(["toggle","show","hide"],function(t,n){var o=x.fn[n];x.fn[n]=function(t,e,i){return null==t||"boolean"==typeof t?o.apply(this,arguments):this.animate(ye(n,!0),t,e,i)}}),x.each({slideDown:ye("show"),slideUp:ye("hide"),slideToggle:ye("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(t,n){x.fn[t]=function(t,e,i){return this.animate(n,t,e,i)}}),x.timers=[],x.fx.tick=function(){var t,e=0,i=x.timers;for(O=Date.now();e<i.length;e++)(t=i[e])()||i[e]!==t||i.splice(e--,1);i.length||x.fx.stop(),O=void 0},x.fx.timer=function(t){x.timers.push(t),x.fx.start()},x.fx.interval=13,x.fx.start=function(){he||(he=!0,me())},x.fx.stop=function(){he=null},x.fx.speeds={slow:600,fast:200,_default:400},x.fn.delay=function(n,t){return n=x.fx&&x.fx.speeds[n]||n,this.queue(t=t||"fx",function(t,e){var i=_.setTimeout(t,n);e.stop=function(){_.clearTimeout(i)}})},$=k.createElement("input"),t=k.createElement("select").appendChild(k.createElement("option")),$.type="checkbox",g.checkOn=""!==$.value,g.optSelected=t.selected,($=k.createElement("input")).value="t",$.type="radio",g.radioValue="t"===$.value;var be,we=x.expr.attrHandle,_e=(x.fn.extend({attr:function(t,e){return u(this,x.attr,t,e,1<arguments.length)},removeAttr:function(t){return this.each(function(){x.removeAttr(this,t)})}}),x.extend({attr:function(t,e,i){var n,o,s=t.nodeType;if(3!==s&&8!==s&&2!==s)return void 0===t.getAttribute?x.prop(t,e,i):(1===s&&x.isXMLDoc(t)||(o=x.attrHooks[e.toLowerCase()]||(x.expr.match.bool.test(e)?be:void 0)),void 0!==i?null===i?void x.removeAttr(t,e):o&&"set"in o&&void 0!==(n=o.set(t,i,e))?n:(t.setAttribute(e,i+""),i):!(o&&"get"in o&&null!==(n=o.get(t,e)))&&null==(n=x.find.attr(t,e))?void 0:n)},attrHooks:{type:{set:function(t,e){var i;if(!g.radioValue&&"radio"===e&&l(t,"input"))return i=t.value,t.setAttribute("type",e),i&&(t.value=i),e}}},removeAttr:function(t,e){var i,n=0,o=e&&e.match(T);if(o&&1===t.nodeType)for(;i=o[n++];)t.removeAttribute(i)}}),be={set:function(t,e,i){return!1===e?x.removeAttr(t,i):t.setAttribute(i,i),i}},x.each(x.expr.match.bool.source.match(/\w+/g),function(t,e){var r=we[e]||x.find.attr;we[e]=function(t,e,i){var n,o,s=e.toLowerCase();return i||(o=we[s],we[s]=n,n=null!=r(t,e,i)?s:null,we[s]=o),n}}),/^(?:input|select|textarea|button)$/i),ke=/^(?:a|area)$/i;function j(t){return(t.match(T)||[]).join(" ")}function P(t){return t.getAttribute&&t.getAttribute("class")||""}function xe(t){return Array.isArray(t)?t:"string"==typeof t&&t.match(T)||[]}x.fn.extend({prop:function(t,e){return u(this,x.prop,t,e,1<arguments.length)},removeProp:function(t){return this.each(function(){delete this[x.propFix[t]||t]})}}),x.extend({prop:function(t,e,i){var n,o,s=t.nodeType;if(3!==s&&8!==s&&2!==s)return 1===s&&x.isXMLDoc(t)||(e=x.propFix[e]||e,o=x.propHooks[e]),void 0!==i?o&&"set"in o&&void 0!==(n=o.set(t,i,e))?n:t[e]=i:o&&"get"in o&&null!==(n=o.get(t,e))?n:t[e]},propHooks:{tabIndex:{get:function(t){var e=x.find.attr(t,"tabindex");return e?parseInt(e,10):_e.test(t.nodeName)||ke.test(t.nodeName)&&t.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),g.optSelected||(x.propHooks.selected={get:function(t){t=t.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(t){t=t.parentNode;t&&(t.selectedIndex,t.parentNode)&&t.parentNode.selectedIndex}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){x.propFix[this.toLowerCase()]=this}),x.fn.extend({addClass:function(e){var t,i,n,o,s,r;return v(e)?this.each(function(t){x(this).addClass(e.call(this,t,P(this)))}):(t=xe(e)).length?this.each(function(){if(n=P(this),i=1===this.nodeType&&" "+j(n)+" "){for(s=0;s<t.length;s++)o=t[s],i.indexOf(" "+o+" ")<0&&(i+=o+" ");r=j(i),n!==r&&this.setAttribute("class",r)}}):this},removeClass:function(e){var t,i,n,o,s,r;return v(e)?this.each(function(t){x(this).removeClass(e.call(this,t,P(this)))}):arguments.length?(t=xe(e)).length?this.each(function(){if(n=P(this),i=1===this.nodeType&&" "+j(n)+" "){for(s=0;s<t.length;s++)for(o=t[s];-1<i.indexOf(" "+o+" ");)i=i.replace(" "+o+" "," ");r=j(i),n!==r&&this.setAttribute("class",r)}}):this:this.attr("class","")},toggleClass:function(e,i){var t,n,o,s,r=typeof e,a="string"==r||Array.isArray(e);return v(e)?this.each(function(t){x(this).toggleClass(e.call(this,t,P(this),i),i)}):"boolean"==typeof i&&a?i?this.addClass(e):this.removeClass(e):(t=xe(e),this.each(function(){if(a)for(s=x(this),o=0;o<t.length;o++)n=t[o],s.hasClass(n)?s.removeClass(n):s.addClass(n);else void 0!==e&&"boolean"!=r||((n=P(this))&&w.set(this,"__className__",n),this.setAttribute&&this.setAttribute("class",!n&&!1!==e&&w.get(this,"__className__")||""))}))},hasClass:function(t){for(var e,i=0,n=" "+t+" ";e=this[i++];)if(1===e.nodeType&&-1<(" "+j(P(e))+" ").indexOf(n))return!0;return!1}});function Te(t){t.stopPropagation()}var Se=/\r/g,Ce=(x.fn.extend({val:function(e){var i,t,n,o=this[0];return arguments.length?(n=v(e),this.each(function(t){1!==this.nodeType||(null==(t=n?e.call(this,t,x(this).val()):e)?t="":"number"==typeof t?t+="":Array.isArray(t)&&(t=x.map(t,function(t){return null==t?"":t+""})),(i=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()])&&"set"in i&&void 0!==i.set(this,t,"value"))||(this.value=t)})):o?(i=x.valHooks[o.type]||x.valHooks[o.nodeName.toLowerCase()])&&"get"in i&&void 0!==(t=i.get(o,"value"))?t:"string"==typeof(t=o.value)?t.replace(Se,""):null==t?"":t:void 0}}),x.extend({valHooks:{option:{get:function(t){var e=x.find.attr(t,"value");return null!=e?e:j(x.text(t))}},select:{get:function(t){for(var e,i=t.options,n=t.selectedIndex,o="select-one"===t.type,s=o?null:[],r=o?n+1:i.length,a=n<0?r:o?n:0;a<r;a++)if(((e=i[a]).selected||a===n)&&!e.disabled&&(!e.parentNode.disabled||!l(e.parentNode,"optgroup"))){if(e=x(e).val(),o)return e;s.push(e)}return s},set:function(t,e){for(var i,n,o=t.options,s=x.makeArray(e),r=o.length;r--;)((n=o[r]).selected=-1<x.inArray(x.valHooks.option.get(n),s))&&(i=!0);return i||(t.selectedIndex=-1),s}}}}),x.each(["radio","checkbox"],function(){x.valHooks[this]={set:function(t,e){if(Array.isArray(e))return t.checked=-1<x.inArray(x(t).val(),e)}},g.checkOn||(x.valHooks[this].get=function(t){return null===t.getAttribute("value")?"on":t.value})}),g.focusin="onfocusin"in _,/^(?:focusinfocus|focusoutblur)$/),Ee=(x.extend(x.event,{trigger:function(t,e,i,n){var o,s,r,a,l,c,d,u=[i||k],h=W.call(t,"type")?t.type:t,p=W.call(t,"namespace")?t.namespace.split("."):[],f=d=s=i=i||k;if(3!==i.nodeType&&8!==i.nodeType&&!Ce.test(h+x.event.triggered)&&(-1<h.indexOf(".")&&(h=(p=h.split(".")).shift(),p.sort()),a=h.indexOf(":")<0&&"on"+h,(t=t[x.expando]?t:new x.Event(h,"object"==typeof t&&t)).isTrigger=n?2:3,t.namespace=p.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=i),e=null==e?[t]:x.makeArray(e,[t]),c=x.event.special[h]||{},n||!c.trigger||!1!==c.trigger.apply(i,e))){if(!n&&!c.noBubble&&!m(i)){for(r=c.delegateType||h,Ce.test(r+h)||(f=f.parentNode);f;f=f.parentNode)u.push(f),s=f;s===(i.ownerDocument||k)&&u.push(s.defaultView||s.parentWindow||_)}for(o=0;(f=u[o++])&&!t.isPropagationStopped();)d=f,t.type=1<o?r:c.bindType||h,(l=(w.get(f,"events")||Object.create(null))[t.type]&&w.get(f,"handle"))&&l.apply(f,e),(l=a&&f[a])&&l.apply&&y(f)&&(t.result=l.apply(f,e),!1===t.result)&&t.preventDefault();return t.type=h,n||t.isDefaultPrevented()||c._default&&!1!==c._default.apply(u.pop(),e)||!y(i)||a&&v(i[h])&&!m(i)&&((s=i[a])&&(i[a]=null),x.event.triggered=h,t.isPropagationStopped()&&d.addEventListener(h,Te),i[h](),t.isPropagationStopped()&&d.removeEventListener(h,Te),x.event.triggered=void 0,s)&&(i[a]=s),t.result}},simulate:function(t,e,i){i=x.extend(new x.Event,i,{type:t,isSimulated:!0});x.event.trigger(i,null,e)}}),x.fn.extend({trigger:function(t,e){return this.each(function(){x.event.trigger(t,e,this)})},triggerHandler:function(t,e){var i=this[0];if(i)return x.event.trigger(t,e,i,!0)}}),g.focusin||x.each({focus:"focusin",blur:"focusout"},function(i,n){function o(t){x.event.simulate(n,t.target,x.event.fix(t))}x.event.special[n]={setup:function(){var t=this.ownerDocument||this.document||this,e=w.access(t,n);e||t.addEventListener(i,o,!0),w.access(t,n,(e||0)+1)},teardown:function(){var t=this.ownerDocument||this.document||this,e=w.access(t,n)-1;e?w.access(t,n,e):(t.removeEventListener(i,o,!0),w.remove(t,n))}}}),_.location),De={guid:Date.now()},Ae=/\?/,Le=(x.parseXML=function(t){var e,i;if(!t||"string"!=typeof t)return null;try{e=(new _.DOMParser).parseFromString(t,"text/xml")}catch(t){}return i=e&&e.getElementsByTagName("parsererror")[0],e&&!i||x.error("Invalid XML: "+(i?x.map(i.childNodes,function(t){return t.textContent}).join("\n"):t)),e},/\[\]$/),Oe=/\r?\n/g,$e=/^(?:submit|button|image|reset|file)$/i,Me=/^(?:input|select|textarea|keygen)/i;x.param=function(t,e){function i(t,e){e=v(e)?e():e,o[o.length]=encodeURIComponent(t)+"="+encodeURIComponent(null==e?"":e)}var n,o=[];if(null==t)return"";if(Array.isArray(t)||t.jquery&&!x.isPlainObject(t))x.each(t,function(){i(this.name,this.value)});else for(n in t)!function i(n,t,o,s){if(Array.isArray(t))x.each(t,function(t,e){o||Le.test(n)?s(n,e):i(n+"["+("object"==typeof e&&null!=e?t:"")+"]",e,o,s)});else if(o||"object"!==f(t))s(n,t);else for(var e in t)i(n+"["+e+"]",t[e],o,s)}(n,t[n],e,i);return o.join("&")},x.fn.extend({serialize:function(){return x.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var t=x.prop(this,"elements");return t?x.makeArray(t):this}).filter(function(){var t=this.type;return this.name&&!x(this).is(":disabled")&&Me.test(this.nodeName)&&!$e.test(t)&&(this.checked||!_t.test(t))}).map(function(t,e){var i=x(this).val();return null==i?null:Array.isArray(i)?x.map(i,function(t){return{name:e.name,value:t.replace(Oe,"\r\n")}}):{name:e.name,value:i.replace(Oe,"\r\n")}}).get()}});var je=/%20/g,Pe=/#.*$/,Ne=/([?&])_=[^&]*/,Ie=/^(.*?):[ \t]*([^\r\n]*)$/gm,He=/^(?:GET|HEAD)$/,Fe=/^\/\//,Re={},qe={},Ye="*/".concat("*"),We=k.createElement("a");function Be(s){return function(t,e){"string"!=typeof t&&(e=t,t="*");var i,n=0,o=t.toLowerCase().match(T)||[];if(v(e))for(;i=o[n++];)"+"===i[0]?(i=i.slice(1)||"*",(s[i]=s[i]||[]).unshift(e)):(s[i]=s[i]||[]).push(e)}}function ze(e,n,o,s){var r={},a=e===qe;function l(t){var i;return r[t]=!0,x.each(e[t]||[],function(t,e){e=e(n,o,s);return"string"!=typeof e||a||r[e]?a?!(i=e):void 0:(n.dataTypes.unshift(e),l(e),!1)}),i}return l(n.dataTypes[0])||!r["*"]&&l("*")}function Ue(t,e){var i,n,o=x.ajaxSettings.flatOptions||{};for(i in e)void 0!==e[i]&&((o[i]?t:n=n||{})[i]=e[i]);return n&&x.extend(!0,t,n),t}We.href=Ee.href,x.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ee.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(Ee.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Ye,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":x.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(t,e){return e?Ue(Ue(t,x.ajaxSettings),e):Ue(x.ajaxSettings,t)},ajaxPrefilter:Be(Re),ajaxTransport:Be(qe),ajax:function(t,e){"object"==typeof t&&(e=t,t=void 0);var l,c,d,i,u,h,p,n,f=x.ajaxSetup({},e=e||{}),m=f.context||f,g=f.context&&(m.nodeType||m.jquery)?x(m):x.event,y=x.Deferred(),v=x.Callbacks("once memory"),b=f.statusCode||{},o={},s={},r="canceled",w={readyState:0,getResponseHeader:function(t){var e;if(h){if(!i)for(i={};e=Ie.exec(d);)i[e[1].toLowerCase()+" "]=(i[e[1].toLowerCase()+" "]||[]).concat(e[2]);e=i[t.toLowerCase()+" "]}return null==e?null:e.join(", ")},getAllResponseHeaders:function(){return h?d:null},setRequestHeader:function(t,e){return null==h&&(t=s[t.toLowerCase()]=s[t.toLowerCase()]||t,o[t]=e),this},overrideMimeType:function(t){return null==h&&(f.mimeType=t),this},statusCode:function(t){if(t)if(h)w.always(t[w.status]);else for(var e in t)b[e]=[b[e],t[e]];return this},abort:function(t){t=t||r;return l&&l.abort(t),a(0,t),this}};if(y.promise(w),f.url=((t||f.url||Ee.href)+"").replace(Fe,Ee.protocol+"//"),f.type=e.method||e.type||f.method||f.type,f.dataTypes=(f.dataType||"*").toLowerCase().match(T)||[""],null==f.crossDomain){t=k.createElement("a");try{t.href=f.url,t.href=t.href,f.crossDomain=We.protocol+"//"+We.host!=t.protocol+"//"+t.host}catch(t){f.crossDomain=!0}}if(f.data&&f.processData&&"string"!=typeof f.data&&(f.data=x.param(f.data,f.traditional)),ze(Re,f,e,w),!h){for(n in(p=x.event&&f.global)&&0==x.active++&&x.event.trigger("ajaxStart"),f.type=f.type.toUpperCase(),f.hasContent=!He.test(f.type),c=f.url.replace(Pe,""),f.hasContent?f.data&&f.processData&&0===(f.contentType||"").indexOf("application/x-www-form-urlencoded")&&(f.data=f.data.replace(je,"+")):(t=f.url.slice(c.length),f.data&&(f.processData||"string"==typeof f.data)&&(c+=(Ae.test(c)?"&":"?")+f.data,delete f.data),!1===f.cache&&(c=c.replace(Ne,"$1"),t=(Ae.test(c)?"&":"?")+"_="+De.guid+++t),f.url=c+t),f.ifModified&&(x.lastModified[c]&&w.setRequestHeader("If-Modified-Since",x.lastModified[c]),x.etag[c])&&w.setRequestHeader("If-None-Match",x.etag[c]),(f.data&&f.hasContent&&!1!==f.contentType||e.contentType)&&w.setRequestHeader("Content-Type",f.contentType),w.setRequestHeader("Accept",f.dataTypes[0]&&f.accepts[f.dataTypes[0]]?f.accepts[f.dataTypes[0]]+("*"!==f.dataTypes[0]?", "+Ye+"; q=0.01":""):f.accepts["*"]),f.headers)w.setRequestHeader(n,f.headers[n]);if(f.beforeSend&&(!1===f.beforeSend.call(m,w,f)||h))return w.abort();if(r="abort",v.add(f.complete),w.done(f.success),w.fail(f.error),l=ze(qe,f,e,w)){if(w.readyState=1,p&&g.trigger("ajaxSend",[w,f]),h)return w;f.async&&0<f.timeout&&(u=_.setTimeout(function(){w.abort("timeout")},f.timeout));try{h=!1,l.send(o,a)}catch(t){if(h)throw t;a(-1,t)}}else a(-1,"No Transport")}return w;function a(t,e,i,n){var o,s,r,a=e;h||(h=!0,u&&_.clearTimeout(u),l=void 0,d=n||"",w.readyState=0<t?4:0,n=200<=t&&t<300||304===t,i&&(r=function(t,e,i){for(var n,o,s,r,a=t.contents,l=t.dataTypes;"*"===l[0];)l.shift(),void 0===n&&(n=t.mimeType||e.getResponseHeader("Content-Type"));if(n)for(o in a)if(a[o]&&a[o].test(n)){l.unshift(o);break}if(l[0]in i)s=l[0];else{for(o in i){if(!l[0]||t.converters[o+" "+l[0]]){s=o;break}r=r||o}s=s||r}if(s)return s!==l[0]&&l.unshift(s),i[s]}(f,w,i)),!n&&-1<x.inArray("script",f.dataTypes)&&x.inArray("json",f.dataTypes)<0&&(f.converters["text script"]=function(){}),r=function(t,e,i,n){var o,s,r,a,l,c={},d=t.dataTypes.slice();if(d[1])for(r in t.converters)c[r.toLowerCase()]=t.converters[r];for(s=d.shift();s;)if(t.responseFields[s]&&(i[t.responseFields[s]]=e),!l&&n&&t.dataFilter&&(e=t.dataFilter(e,t.dataType)),l=s,s=d.shift())if("*"===s)s=l;else if("*"!==l&&l!==s){if(!(r=c[l+" "+s]||c["* "+s]))for(o in c)if((a=o.split(" "))[1]===s&&(r=c[l+" "+a[0]]||c["* "+a[0]])){!0===r?r=c[o]:!0!==c[o]&&(s=a[0],d.unshift(a[1]));break}if(!0!==r)if(r&&t.throws)e=r(e);else try{e=r(e)}catch(t){return{state:"parsererror",error:r?t:"No conversion from "+l+" to "+s}}}return{state:"success",data:e}}(f,r,w,n),n?(f.ifModified&&((i=w.getResponseHeader("Last-Modified"))&&(x.lastModified[c]=i),i=w.getResponseHeader("etag"))&&(x.etag[c]=i),204===t||"HEAD"===f.type?a="nocontent":304===t?a="notmodified":(a=r.state,o=r.data,n=!(s=r.error))):(s=a,!t&&a||(a="error",t<0&&(t=0))),w.status=t,w.statusText=(e||a)+"",n?y.resolveWith(m,[o,a,w]):y.rejectWith(m,[w,a,s]),w.statusCode(b),b=void 0,p&&g.trigger(n?"ajaxSuccess":"ajaxError",[w,f,n?o:s]),v.fireWith(m,[w,a]),p&&(g.trigger("ajaxComplete",[w,f]),--x.active||x.event.trigger("ajaxStop")))}},getJSON:function(t,e,i){return x.get(t,e,i,"json")},getScript:function(t,e){return x.get(t,void 0,e,"script")}}),x.each(["get","post"],function(t,o){x[o]=function(t,e,i,n){return v(e)&&(n=n||i,i=e,e=void 0),x.ajax(x.extend({url:t,type:o,dataType:n,data:e,success:i},x.isPlainObject(t)&&t))}}),x.ajaxPrefilter(function(t){for(var e in t.headers)"content-type"===e.toLowerCase()&&(t.contentType=t.headers[e]||"")}),x._evalUrl=function(t,e,i){return x.ajax({url:t,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(t){x.globalEval(t,e,i)}})},x.fn.extend({wrapAll:function(t){return this[0]&&(v(t)&&(t=t.call(this[0])),t=x(t,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var t=this;t.firstElementChild;)t=t.firstElementChild;return t}).append(this)),this},wrapInner:function(i){return v(i)?this.each(function(t){x(this).wrapInner(i.call(this,t))}):this.each(function(){var t=x(this),e=t.contents();e.length?e.wrapAll(i):t.append(i)})},wrap:function(e){var i=v(e);return this.each(function(t){x(this).wrapAll(i?e.call(this,t):e)})},unwrap:function(t){return this.parent(t).not("body").each(function(){x(this).replaceWith(this.childNodes)}),this}}),x.expr.pseudos.hidden=function(t){return!x.expr.pseudos.visible(t)},x.expr.pseudos.visible=function(t){return!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)},x.ajaxSettings.xhr=function(){try{return new _.XMLHttpRequest}catch(t){}};var Ge={0:200,1223:204},Ve=x.ajaxSettings.xhr(),Xe=(g.cors=!!Ve&&"withCredentials"in Ve,g.ajax=Ve=!!Ve,x.ajaxTransport(function(o){var s,r;if(g.cors||Ve&&!o.crossDomain)return{send:function(t,e){var i,n=o.xhr();if(n.open(o.type,o.url,o.async,o.username,o.password),o.xhrFields)for(i in o.xhrFields)n[i]=o.xhrFields[i];for(i in o.mimeType&&n.overrideMimeType&&n.overrideMimeType(o.mimeType),o.crossDomain||t["X-Requested-With"]||(t["X-Requested-With"]="XMLHttpRequest"),t)n.setRequestHeader(i,t[i]);s=function(t){return function(){s&&(s=r=n.onload=n.onerror=n.onabort=n.ontimeout=n.onreadystatechange=null,"abort"===t?n.abort():"error"===t?"number"!=typeof n.status?e(0,"error"):e(n.status,n.statusText):e(Ge[n.status]||n.status,n.statusText,"text"!==(n.responseType||"text")||"string"!=typeof n.responseText?{binary:n.response}:{text:n.responseText},n.getAllResponseHeaders()))}},n.onload=s(),r=n.onerror=n.ontimeout=s("error"),void 0!==n.onabort?n.onabort=r:n.onreadystatechange=function(){4===n.readyState&&_.setTimeout(function(){s&&r()})},s=s("abort");try{n.send(o.hasContent&&o.data||null)}catch(t){if(s)throw t}},abort:function(){s&&s()}}}),x.ajaxPrefilter(function(t){t.crossDomain&&(t.contents.script=!1)}),x.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(t){return x.globalEval(t),t}}}),x.ajaxPrefilter("script",function(t){void 0===t.cache&&(t.cache=!1),t.crossDomain&&(t.type="GET")}),x.ajaxTransport("script",function(i){var n,o;if(i.crossDomain||i.scriptAttrs)return{send:function(t,e){n=x("<script>").attr(i.scriptAttrs||{}).prop({charset:i.scriptCharset,src:i.url}).on("load error",o=function(t){n.remove(),o=null,t&&e("error"===t.type?404:200,t.type)}),k.head.appendChild(n[0])},abort:function(){o&&o()}}}),[]),Ze=/(=)\?(?=&|$)|\?\?/,Qe=(x.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var t=Xe.pop()||x.expando+"_"+De.guid++;return this[t]=!0,t}}),x.ajaxPrefilter("json jsonp",function(t,e,i){var n,o,s,r=!1!==t.jsonp&&(Ze.test(t.url)?"url":"string"==typeof t.data&&0===(t.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ze.test(t.data)&&"data");if(r||"jsonp"===t.dataTypes[0])return n=t.jsonpCallback=v(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,r?t[r]=t[r].replace(Ze,"$1"+n):!1!==t.jsonp&&(t.url+=(Ae.test(t.url)?"&":"?")+t.jsonp+"="+n),t.converters["script json"]=function(){return s||x.error(n+" was not called"),s[0]},t.dataTypes[0]="json",o=_[n],_[n]=function(){s=arguments},i.always(function(){void 0===o?x(_).removeProp(n):_[n]=o,t[n]&&(t.jsonpCallback=e.jsonpCallback,Xe.push(n)),s&&v(o)&&o(s[0]),s=o=void 0}),"script"}),g.createHTMLDocument=((s=k.implementation.createHTMLDocument("").body).innerHTML="<form></form><form></form>",2===s.childNodes.length),x.parseHTML=function(t,e,i){var n;return"string"!=typeof t?[]:("boolean"==typeof e&&(i=e,e=!1),e||(g.createHTMLDocument?((n=(e=k.implementation.createHTMLDocument("")).createElement("base")).href=k.location.href,e.head.appendChild(n)):e=k),n=!i&&[],(i=Q.exec(t))?[e.createElement(i[1])]:(i=Ct([t],e,n),n&&n.length&&x(n).remove(),x.merge([],i.childNodes)))},x.fn.load=function(t,e,i){var n,o,s,r=this,a=t.indexOf(" ");return-1<a&&(n=j(t.slice(a)),t=t.slice(0,a)),v(e)?(i=e,e=void 0):e&&"object"==typeof e&&(o="POST"),0<r.length&&x.ajax({url:t,type:o||"GET",dataType:"html",data:e}).done(function(t){s=arguments,r.html(n?x("<div>").append(x.parseHTML(t)).find(n):t)}).always(i&&function(t,e){r.each(function(){i.apply(this,s||[t.responseText,e,t])})}),this},x.expr.pseudos.animated=function(e){return x.grep(x.timers,function(t){return e===t.elem}).length},x.offset={setOffset:function(t,e,i){var n,o,s,r,a=x.css(t,"position"),l=x(t),c={};"static"===a&&(t.style.position="relative"),s=l.offset(),n=x.css(t,"top"),r=x.css(t,"left"),a=("absolute"===a||"fixed"===a)&&-1<(n+r).indexOf("auto")?(o=(a=l.position()).top,a.left):(o=parseFloat(n)||0,parseFloat(r)||0),null!=(e=v(e)?e.call(t,i,x.extend({},s)):e).top&&(c.top=e.top-s.top+o),null!=e.left&&(c.left=e.left-s.left+a),"using"in e?e.using.call(t,c):l.css(c)}},x.fn.extend({offset:function(e){var t,i;return arguments.length?void 0===e?this:this.each(function(t){x.offset.setOffset(this,e,t)}):(i=this[0])?i.getClientRects().length?(t=i.getBoundingClientRect(),i=i.ownerDocument.defaultView,{top:t.top+i.pageYOffset,left:t.left+i.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var t,e,i,n=this[0],o={top:0,left:0};if("fixed"===x.css(n,"position"))e=n.getBoundingClientRect();else{for(e=this.offset(),i=n.ownerDocument,t=n.offsetParent||i.documentElement;t&&(t===i.body||t===i.documentElement)&&"static"===x.css(t,"position");)t=t.parentNode;t&&t!==n&&1===t.nodeType&&((o=x(t).offset()).top+=x.css(t,"borderTopWidth",!0),o.left+=x.css(t,"borderLeftWidth",!0))}return{top:e.top-o.top-x.css(n,"marginTop",!0),left:e.left-o.left-x.css(n,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){for(var t=this.offsetParent;t&&"static"===x.css(t,"position");)t=t.offsetParent;return t||S})}}),x.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,o){var s="pageYOffset"===o;x.fn[e]=function(t){return u(this,function(t,e,i){var n;if(m(t)?n=t:9===t.nodeType&&(n=t.defaultView),void 0===i)return n?n[o]:t[e];n?n.scrollTo(s?n.pageXOffset:i,s?i:n.pageYOffset):t[e]=i},e,t,arguments.length)}}),x.each(["top","left"],function(t,i){x.cssHooks[i]=ee(g.pixelPosition,function(t,e){if(e)return e=te(t,i),Vt.test(e)?x(t).position()[i]+"px":e})}),x.each({Height:"height",Width:"width"},function(r,a){x.each({padding:"inner"+r,content:a,"":"outer"+r},function(n,s){x.fn[s]=function(t,e){var i=arguments.length&&(n||"boolean"!=typeof t),o=n||(!0===t||!0===e?"margin":"border");return u(this,function(t,e,i){var n;return m(t)?0===s.indexOf("outer")?t["inner"+r]:t.document.documentElement["client"+r]:9===t.nodeType?(n=t.documentElement,Math.max(t.body["scroll"+r],n["scroll"+r],t.body["offset"+r],n["offset"+r],n["client"+r])):void 0===i?x.css(t,e,o):x.style(t,e,i,o)},a,i?t:void 0,i)}})}),x.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(t,e){x.fn[e]=function(t){return this.on(e,t)}}),x.fn.extend({bind:function(t,e,i){return this.on(t,null,e,i)},unbind:function(t,e){return this.off(t,null,e)},delegate:function(t,e,i,n){return this.on(e,t,i,n)},undelegate:function(t,e,i){return 1===arguments.length?this.off(t,"**"):this.off(e,t||"**",i)},hover:function(t,e){return this.mouseenter(t).mouseleave(e||t)}}),x.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(t,i){x.fn[i]=function(t,e){return 0<arguments.length?this.on(i,null,t,e):this.trigger(i)}}),/^[\s\uFEFF\xA0]+|([^\s\uFEFF\xA0])[\s\uFEFF\xA0]+$/g),Je=(x.proxy=function(t,e){var i,n;if("string"==typeof e&&(n=t[e],e=t,t=n),v(t))return i=a.call(arguments,2),(n=function(){return t.apply(e||this,i.concat(a.call(arguments)))}).guid=t.guid=t.guid||x.guid++,n},x.holdReady=function(t){t?x.readyWait++:x.ready(!0)},x.isArray=Array.isArray,x.parseJSON=JSON.parse,x.nodeName=l,x.isFunction=v,x.isWindow=m,x.camelCase=b,x.type=f,x.now=Date.now,x.isNumeric=function(t){var e=x.type(t);return("number"===e||"string"===e)&&!isNaN(t-parseFloat(t))},x.trim=function(t){return null==t?"":(t+"").replace(Qe,"$1")},"function"==typeof define&&define.amd&&define("jquery",[],function(){return x}),_.jQuery),Ke=_.$;return x.noConflict=function(t){return _.$===x&&(_.$=Ke),t&&_.jQuery===x&&(_.jQuery=Je),x},void 0===N&&(_.jQuery=_.$=x),x}),!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,function(){"use strict";const N="transitionend",I=t=>{for(;t+=Math.floor(1e6*Math.random()),document.getElementById(t););return t},H=e=>{let i=e.getAttribute("data-bs-target");if(!i||"#"===i){let t=e.getAttribute("href");if(!t||!t.includes("#")&&!t.startsWith("."))return null;t.includes("#")&&!t.startsWith("#")&&(t="#"+t.split("#")[1]),i=t&&"#"!==t?t.trim():null}return i},F=t=>{t=H(t);return t&&document.querySelector(t)?t:null},o=t=>{t=H(t);return t?document.querySelector(t):null},d=t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);var t=Number.parseFloat(e),n=Number.parseFloat(i);return t||n?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0},R=t=>{t.dispatchEvent(new Event(N))},r=t=>(t[0]||t).nodeType,u=(e,t)=>{let i=!1;t+=5;e.addEventListener(N,function t(){i=!0,e.removeEventListener(N,t)}),setTimeout(()=>{i||R(e)},t)},i=(n,o,s)=>{Object.keys(s).forEach(t=>{var e=s[t],i=o[t],i=i&&r(i)?"element":null==(i=i)?""+i:{}.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(e).test(i))throw new TypeError(n.toUpperCase()+": "+`Option "${t}" provided type "${i}" `+`but expected type "${e}".`)})},q=t=>{var e;return!!t&&!!(t.style&&t.parentNode&&t.parentNode.style)&&(e=getComputedStyle(t),t=getComputedStyle(t.parentNode),"none"!==e.display)&&"none"!==t.display&&"hidden"!==e.visibility},Y=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),W=t=>{var e;return document.documentElement.attachShadow?"function"==typeof t.getRootNode?(e=t.getRootNode())instanceof ShadowRoot?e:null:t instanceof ShadowRoot?t:t.parentNode?W(t.parentNode):null:null},B=()=>function(){},z=t=>t.offsetHeight,U=()=>{var t=window["jQuery"];return t&&!document.body.hasAttribute("data-bs-no-jquery")?t:null},e=()=>"rtl"===document.documentElement.dir;var t=(i,n)=>{var t;t=()=>{const t=U();if(t){const e=t.fn[i];t.fn[i]=n.jQueryInterface,t.fn[i].Constructor=n,t.fn[i].noConflict=()=>(t.fn[i]=e,n.jQueryInterface)}},"loading"===document.readyState?document.addEventListener("DOMContentLoaded",t):t()};const n=new Map;var G=function(t,e,i){n.has(t)||n.set(t,new Map);t=n.get(t);t.has(e)||0===t.size?t.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(t.keys())[0]}.`)},a=function(t,e){return n.has(t)&&n.get(t).get(e)||null},V=function(t,e){var i;n.has(t)&&((i=n.get(t)).delete(e),0===i.size)&&n.delete(t)};const X=/[^.]*(?=\..*)\.|.*/,Z=/\..*/,Q=/::\d+$/,J={};let K=1;const tt={mouseenter:"mouseover",mouseleave:"mouseout"},et=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function it(t,e){return e&&e+"::"+K++||t.uidEvent||K++}function nt(t){var e=it(t);return t.uidEvent=e,J[e]=J[e]||{},J[e]}function ot(i,n,o=null){var s=Object.keys(i);for(let t=0,e=s.length;t<e;t++){var r=i[s[t]];if(r.originalHandler===n&&r.delegationSelector===o)return r}return null}function st(t,e,i){var n="string"==typeof e,i=n?i:e;let o=t.replace(Z,"");e=tt[o],e&&(o=e),e=et.has(o);return[n,i,o=e?o:t]}function rt(t,e,i,n,o){var s,r,a,l,c,d,u,h,p,f;"string"==typeof e&&t&&(i||(i=n,n=null),[s,r,a]=st(e,i,n),(c=ot(l=(l=nt(t))[a]||(l[a]={}),r,s?i:null))?c.oneOff=c.oneOff&&o:(c=it(r,e.replace(X,"")),(e=s?(h=t,p=i,f=n,function i(n){var o=h.querySelectorAll(p);for(let e=n["target"];e&&e!==this;e=e.parentNode)for(let t=o.length;t--;)if(o[t]===e)return n.delegateTarget=e,i.oneOff&&m.off(h,n.type,f),f.apply(e,[n]);return null}):(d=t,u=i,function t(e){return e.delegateTarget=d,t.oneOff&&m.off(d,e.type,u),u.apply(d,[e])})).delegationSelector=s?i:null,e.originalHandler=r,e.oneOff=o,l[e.uidEvent=c]=e,t.addEventListener(a,e,s)))}function at(t,e,i,n,o){n=ot(e[i],n,o);n&&(t.removeEventListener(i,n,Boolean(o)),delete e[i][n.uidEvent])}const m={on(t,e,i,n){rt(t,e,i,n,!1)},one(t,e,i,n){rt(t,e,i,n,!0)},off(r,a,t,e){if("string"==typeof a&&r){const[i,n,o]=st(a,t,e),s=o!==a,l=nt(r);e=a.startsWith(".");if(void 0!==n)return l&&l[o]?void at(r,l,o,n,i?t:null):void 0;e&&Object.keys(l).forEach(t=>{{var e=r,i=l,n=t,o=a.slice(1);const s=i[n]||{};Object.keys(s).forEach(t=>{t.includes(o)&&(t=s[t],at(e,i,n,t.originalHandler,t.delegationSelector))})}});const c=l[o]||{};Object.keys(c).forEach(t=>{var e=t.replace(Q,"");s&&!a.includes(e)||(e=c[t],at(r,l,o,e.originalHandler,e.delegationSelector))})}},trigger(t,e,i){if("string"!=typeof e||!t)return null;var n=U(),o=e.replace(Z,""),s=e!==o,r=et.has(o);let a,l=!0,c=!0,d=!1,u=null;return s&&n&&(a=n.Event(e,i),n(t).trigger(a),l=!a.isPropagationStopped(),c=!a.isImmediatePropagationStopped(),d=a.isDefaultPrevented()),r?(u=document.createEvent("HTMLEvents")).initEvent(o,l,!0):u=new CustomEvent(e,{bubbles:l,cancelable:!0}),void 0!==i&&Object.keys(i).forEach(t=>{Object.defineProperty(u,t,{get(){return i[t]}})}),d&&u.preventDefault(),c&&t.dispatchEvent(u),u.defaultPrevented&&void 0!==a&&a.preventDefault(),u}};class s{constructor(t){(t="string"==typeof t?document.querySelector(t):t)&&(this._element=t,G(this._element,this.constructor.DATA_KEY,this))}dispose(){V(this._element,this.constructor.DATA_KEY),this._element=null}static getInstance(t){return a(t,this.DATA_KEY)}static get VERSION(){return"5.0.0-beta3"}}const lt="bs.alert";lt;class ct extends s{static get DATA_KEY(){return lt}close(t){var t=t?this._getRootElement(t):this._element,e=this._triggerCloseEvent(t);null===e||e.defaultPrevented||this._removeElement(t)}_getRootElement(t){return o(t)||t.closest(".alert")}_triggerCloseEvent(t){return m.trigger(t,"close.bs.alert")}_removeElement(t){var e;t.classList.remove("show"),t.classList.contains("fade")?(e=d(t),m.one(t,"transitionend",()=>this._destroyElement(t)),u(t,e)):this._destroyElement(t)}_destroyElement(t){t.parentNode&&t.parentNode.removeChild(t),m.trigger(t,"closed.bs.alert")}static jQueryInterface(e){return this.each(function(){let t=a(this,lt);t=t||new ct(this),"close"===e&&t[e](this)})}static handleDismiss(e){return function(t){t&&t.preventDefault(),e.close(this)}}}m.on(document,"click.bs.alert.data-api",'[data-bs-dismiss="alert"]',ct.handleDismiss(new ct)),t("alert",ct);const dt="bs.button";dt;const ut='[data-bs-toggle="button"]';class ht extends s{static get DATA_KEY(){return dt}toggle(){this._element.setAttribute("aria-pressed",this._element.classList.toggle("active"))}static jQueryInterface(e){return this.each(function(){let t=a(this,dt);t=t||new ht(this),"toggle"===e&&t[e]()})}}function pt(t){return"true"===t||"false"!==t&&(t===Number(t).toString()?Number(t):""===t||"null"===t?null:t)}function ft(t){return t.replace(/[A-Z]/g,t=>"-"+t.toLowerCase())}m.on(document,"click.bs.button.data-api",ut,t=>{t.preventDefault();t=t.target.closest(ut);let e=a(t,dt);(e=e||new ht(t)).toggle()}),t("button",ht);const l={setDataAttribute(t,e,i){t.setAttribute("data-bs-"+ft(e),i)},removeDataAttribute(t,e){t.removeAttribute("data-bs-"+ft(e))},getDataAttributes(i){if(!i)return{};const n={};return Object.keys(i.dataset).filter(t=>t.startsWith("bs")).forEach(t=>{let e=t.replace(/^bs/,"");e=e.charAt(0).toLowerCase()+e.slice(1,e.length),n[e]=pt(i.dataset[t])}),n},getDataAttribute(t,e){return pt(t.getAttribute("data-bs-"+ft(e)))},offset(t){t=t.getBoundingClientRect();return{top:t.top+document.body.scrollTop,left:t.left+document.body.scrollLeft}},position(t){return{top:t.offsetTop,left:t.offsetLeft}}},h={find(t,e=document.documentElement){return[].concat(...Element.prototype.querySelectorAll.call(e,t))},findOne(t,e=document.documentElement){return Element.prototype.querySelector.call(e,t)},children(t,e){return[].concat(...t.children).filter(t=>t.matches(e))},parents(t,e){var i=[];let n=t.parentNode;for(;n&&n.nodeType===Node.ELEMENT_NODE&&3!==n.nodeType;)n.matches(e)&&i.push(n),n=n.parentNode;return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]}},mt="carousel",gt="bs.carousel",c="."+gt;const yt={interval:5e3,keyboard:!0,slide:!1,pause:"hover",wrap:!0,touch:!0},vt={interval:"(number|boolean)",keyboard:"boolean",slide:"(boolean|string)",pause:"(string|boolean)",wrap:"boolean",touch:"boolean"},p="next",bt="prev",wt="left",f="right",_t=(c,"slid"+c);c,c,c,c,c,c,c,c,c;c,c;const g="active",kt=".active.carousel-item";class y extends s{constructor(t,e){super(t),this._items=null,this._interval=null,this._activeElement=null,this._isPaused=!1,this._isSliding=!1,this.touchTimeout=null,this.touchStartX=0,this.touchDeltaX=0,this._config=this._getConfig(e),this._indicatorsElement=h.findOne(".carousel-indicators",this._element),this._touchSupported="ontouchstart"in document.documentElement||0<navigator.maxTouchPoints,this._pointerEvent=Boolean(window.PointerEvent),this._addEventListeners()}static get Default(){return yt}static get DATA_KEY(){return gt}next(){this._isSliding||this._slide(p)}nextWhenVisible(){!document.hidden&&q(this._element)&&this.next()}prev(){this._isSliding||this._slide(bt)}pause(t){t||(this._isPaused=!0),h.findOne(".carousel-item-next, .carousel-item-prev",this._element)&&(R(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null}cycle(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config&&this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))}to(t){this._activeElement=h.findOne(kt,this._element);var e=this._getItemIndex(this._activeElement);t>this._items.length-1||t<0||(this._isSliding?m.one(this._element,_t,()=>this.to(t)):e===t?(this.pause(),this.cycle()):(e=e<t?p:bt,this._slide(e,this._items[t])))}dispose(){m.off(this._element,c),this._items=null,this._config=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null,super.dispose()}_getConfig(t){return t={...yt,...t},i(mt,t,vt),t}_handleSwipe(){var t=Math.abs(this.touchDeltaX);t<=40||(t=t/this.touchDeltaX,this.touchDeltaX=0,t&&this._slide(0<t?f:wt))}_addEventListeners(){this._config.keyboard&&m.on(this._element,"keydown.bs.carousel",t=>this._keydown(t)),"hover"===this._config.pause&&(m.on(this._element,"mouseenter.bs.carousel",t=>this.pause(t)),m.on(this._element,"mouseleave.bs.carousel",t=>this.cycle(t))),this._config.touch&&this._touchSupported&&this._addTouchEventListeners()}_addTouchEventListeners(){const e=t=>{!this._pointerEvent||"pen"!==t.pointerType&&"touch"!==t.pointerType?this._pointerEvent||(this.touchStartX=t.touches[0].clientX):this.touchStartX=t.clientX},i=t=>{this.touchDeltaX=t.touches&&1<t.touches.length?0:t.touches[0].clientX-this.touchStartX},n=t=>{!this._pointerEvent||"pen"!==t.pointerType&&"touch"!==t.pointerType||(this.touchDeltaX=t.clientX-this.touchStartX),this._handleSwipe(),"hover"===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout(t=>this.cycle(t),500+this._config.interval))};h.find(".carousel-item img",this._element).forEach(t=>{m.on(t,"dragstart.bs.carousel",t=>t.preventDefault())}),this._pointerEvent?(m.on(this._element,"pointerdown.bs.carousel",t=>e(t)),m.on(this._element,"pointerup.bs.carousel",t=>n(t)),this._element.classList.add("pointer-event")):(m.on(this._element,"touchstart.bs.carousel",t=>e(t)),m.on(this._element,"touchmove.bs.carousel",t=>i(t)),m.on(this._element,"touchend.bs.carousel",t=>n(t)))}_keydown(t){/input|textarea/i.test(t.target.tagName)||("ArrowLeft"===t.key?(t.preventDefault(),this._slide(wt)):"ArrowRight"===t.key&&(t.preventDefault(),this._slide(f)))}_getItemIndex(t){return this._items=t&&t.parentNode?h.find(".carousel-item",t.parentNode):[],this._items.indexOf(t)}_getItemByOrder(t,e){var i=t===p,t=t===bt,n=this._getItemIndex(e),o=this._items.length-1;return(t&&0===n||i&&n===o)&&!this._config.wrap?e:-1==(i=(n+(t?-1:1))%this._items.length)?this._items[this._items.length-1]:this._items[i]}_triggerSlideEvent(t,e){var i=this._getItemIndex(t),n=this._getItemIndex(h.findOne(kt,this._element));return m.trigger(this._element,"slide.bs.carousel",{relatedTarget:t,direction:e,from:n,to:i})}_setActiveIndicatorElement(e){if(this._indicatorsElement){var t=h.findOne(".active",this._indicatorsElement),i=(t.classList.remove(g),t.removeAttribute("aria-current"),h.find("[data-bs-target]",this._indicatorsElement));for(let t=0;t<i.length;t++)if(Number.parseInt(i[t].getAttribute("data-bs-slide-to"),10)===this._getItemIndex(e)){i[t].classList.add(g),i[t].setAttribute("aria-current","true");break}}}_updateInterval(){var t=this._activeElement||h.findOne(kt,this._element);t&&((t=Number.parseInt(t.getAttribute("data-bs-interval"),10))?(this._config.defaultInterval=this._config.defaultInterval||this._config.interval,this._config.interval=t):this._config.interval=this._config.defaultInterval||this._config.interval)}_slide(t,e){t=this._directionToOrder(t);const i=h.findOne(kt,this._element),n=this._getItemIndex(i),o=e||this._getItemByOrder(t,i),s=this._getItemIndex(o);var e=Boolean(this._interval),r=t===p;const a=r?"carousel-item-start":"carousel-item-end",l=r?"carousel-item-next":"carousel-item-prev",c=this._orderToDirection(t);o&&o.classList.contains(g)?this._isSliding=!1:this._triggerSlideEvent(o,c).defaultPrevented||i&&o&&(this._isSliding=!0,e&&this.pause(),this._setActiveIndicatorElement(o),this._activeElement=o,this._element.classList.contains("slide")?(o.classList.add(l),z(o),i.classList.add(a),o.classList.add(a),r=d(i),m.one(i,"transitionend",()=>{o.classList.remove(a,l),o.classList.add(g),i.classList.remove(g,l,a),this._isSliding=!1,setTimeout(()=>{m.trigger(this._element,_t,{relatedTarget:o,direction:c,from:n,to:s})},0)}),u(i,r)):(i.classList.remove(g),o.classList.add(g),this._isSliding=!1,m.trigger(this._element,_t,{relatedTarget:o,direction:c,from:n,to:s})),e)&&this.cycle()}_directionToOrder(t){return[f,wt].includes(t)?e()?t===f?bt:p:t===f?p:bt:t}_orderToDirection(t){return[p,bt].includes(t)?e()?t===p?wt:f:t===p?f:wt:t}static carouselInterface(t,e){let i=a(t,gt),n={...yt,...l.getDataAttributes(t)};"object"==typeof e&&(n={...n,...e});var o="string"==typeof e?e:n.slide;if(i=i||new y(t,n),"number"==typeof e)i.to(e);else if("string"==typeof o){if(void 0===i[o])throw new TypeError(`No method named "${o}"`);i[o]()}else n.interval&&n.ride&&(i.pause(),i.cycle())}static jQueryInterface(t){return this.each(function(){y.carouselInterface(this,t)})}static dataApiClickHandler(t){var e,i,n=o(this);n&&n.classList.contains("carousel")&&(e={...l.getDataAttributes(n),...l.getDataAttributes(this)},(i=this.getAttribute("data-bs-slide-to"))&&(e.interval=!1),y.carouselInterface(n,e),i&&a(n,gt).to(i),t.preventDefault())}}m.on(document,"click.bs.carousel.data-api","[data-bs-slide], [data-bs-slide-to]",y.dataApiClickHandler),m.on(window,"load.bs.carousel.data-api",()=>{var i=h.find('[data-bs-ride="carousel"]');for(let t=0,e=i.length;t<e;t++)y.carouselInterface(i[t],a(i[t],gt))}),t(mt,y);const xt="collapse",Tt="bs.collapse";Tt;const St={toggle:!0,parent:""},Ct={toggle:"boolean",parent:"(string|element)"};const v="show",Et="collapse",Dt="collapsing",At="collapsed",Lt='[data-bs-toggle="collapse"]';class Ot extends s{constructor(t,e){super(t),this._isTransitioning=!1,this._config=this._getConfig(e),this._triggerArray=h.find(Lt+`[href="#${this._element.id}"],`+Lt+`[data-bs-target="#${this._element.id}"]`);var i=h.find(Lt);for(let t=0,e=i.length;t<e;t++){var n=i[t],o=F(n),s=h.find(o).filter(t=>t===this._element);null!==o&&s.length&&(this._selector=o,this._triggerArray.push(n))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}static get Default(){return St}static get DATA_KEY(){return Tt}toggle(){this._element.classList.contains(v)?this.hide():this.show()}show(){if(!this._isTransitioning&&!this._element.classList.contains(v)){let t,e;this._parent&&0===(t=h.find(".show, .collapsing",this._parent).filter(t=>"string"==typeof this._config.parent?t.getAttribute("data-bs-parent")===this._config.parent:t.classList.contains(Et))).length&&(t=null);const o=h.findOne(this._selector);if(t){var i=t.find(t=>o!==t);if((e=i?a(i,Tt):null)&&e._isTransitioning)return}i=m.trigger(this._element,"show.bs.collapse");if(!i.defaultPrevented){t&&t.forEach(t=>{o!==t&&Ot.collapseInterface(t,"hide"),e||G(t,Tt,null)});const s=this._getDimension();this._element.classList.remove(Et),this._element.classList.add(Dt),this._element.style[s]=0,this._triggerArray.length&&this._triggerArray.forEach(t=>{t.classList.remove(At),t.setAttribute("aria-expanded",!0)}),this.setTransitioning(!0);var i="scroll"+(s[0].toUpperCase()+s.slice(1)),n=d(this._element);m.one(this._element,"transitionend",()=>{this._element.classList.remove(Dt),this._element.classList.add(Et,v),this._element.style[s]="",this.setTransitioning(!1),m.trigger(this._element,"shown.bs.collapse")}),u(this._element,n),this._element.style[s]=this._element[i]+"px"}}}hide(){if(!this._isTransitioning&&this._element.classList.contains(v)){var t=m.trigger(this._element,"hide.bs.collapse");if(!t.defaultPrevented){var t=this._getDimension(),e=(this._element.style[t]=this._element.getBoundingClientRect()[t]+"px",z(this._element),this._element.classList.add(Dt),this._element.classList.remove(Et,v),this._triggerArray.length);if(0<e)for(let t=0;t<e;t++){var i=this._triggerArray[t],n=o(i);n&&!n.classList.contains(v)&&(i.classList.add(At),i.setAttribute("aria-expanded",!1))}this.setTransitioning(!0);this._element.style[t]="";t=d(this._element);m.one(this._element,"transitionend",()=>{this.setTransitioning(!1),this._element.classList.remove(Dt),this._element.classList.add(Et),m.trigger(this._element,"hidden.bs.collapse")}),u(this._element,t)}}}setTransitioning(t){this._isTransitioning=t}dispose(){super.dispose(),this._config=null,this._parent=null,this._triggerArray=null,this._isTransitioning=null}_getConfig(t){return(t={...St,...t}).toggle=Boolean(t.toggle),i(xt,t,Ct),t}_getDimension(){return this._element.classList.contains("width")?"width":"height"}_getParent(){let t=this._config["parent"];r(t)?void 0===t.jquery&&void 0===t[0]||(t=t[0]):t=h.findOne(t);var e=Lt+`[data-bs-parent="${t}"]`;return h.find(e,t).forEach(t=>{var e=o(t);this._addAriaAndCollapsedClass(e,[t])}),t}_addAriaAndCollapsedClass(t,e){if(t&&e.length){const i=t.classList.contains(v);e.forEach(t=>{i?t.classList.remove(At):t.classList.add(At),t.setAttribute("aria-expanded",i)})}}static collapseInterface(t,e){let i=a(t,Tt);var n={...St,...l.getDataAttributes(t),..."object"==typeof e&&e?e:{}};if(!i&&n.toggle&&"string"==typeof e&&/show|hide/.test(e)&&(n.toggle=!1),i=i||new Ot(t,n),"string"==typeof e){if(void 0===i[e])throw new TypeError(`No method named "${e}"`);i[e]()}}static jQueryInterface(t){return this.each(function(){Ot.collapseInterface(this,t)})}}m.on(document,"click.bs.collapse.data-api",Lt,function(t){("A"===t.target.tagName||t.delegateTarget&&"A"===t.delegateTarget.tagName)&&t.preventDefault();const n=l.getDataAttributes(this);t=F(this);h.find(t).forEach(t=>{var e=a(t,Tt);let i;i=e?(null===e._parent&&"string"==typeof n.parent&&(e._config.parent=n.parent,e._parent=e._getParent()),"toggle"):n,Ot.collapseInterface(t,i)})}),t(xt,Ot);var E="top",D="bottom",A="right",L="left",$t="auto",Mt=[E,D,A,L],jt="start",Pt="end",Nt="clippingParents",It="viewport",Ht="popper",Ft="reference",Rt=Mt.reduce(function(t,e){return t.concat([e+"-"+jt,e+"-"+Pt])},[]),qt=[].concat(Mt,[$t]).reduce(function(t,e){return t.concat([e,e+"-"+jt,e+"-"+Pt])},[]),Yt="beforeRead",Wt="afterRead",Bt="beforeMain",zt="afterMain",Ut="beforeWrite",Gt="afterWrite",Vt=[Yt,"read",Wt,Bt,"main",zt,Ut,"write",Gt];function b(t){return t?(t.nodeName||"").toLowerCase():null}function w(t){var e;return null==t?window:"[object Window]"!==t.toString()?(e=t.ownerDocument)&&e.defaultView||window:t}function Xt(t){return t instanceof w(t).Element||t instanceof Element}function _(t){return t instanceof w(t).HTMLElement||t instanceof HTMLElement}function Zt(t){return"undefined"!=typeof ShadowRoot&&(t instanceof w(t).ShadowRoot||t instanceof ShadowRoot)}var Qt={name:"applyStyles",enabled:!0,phase:"write",fn:function(t){var o=t.state;Object.keys(o.elements).forEach(function(t){var e=o.styles[t]||{},i=o.attributes[t]||{},n=o.elements[t];_(n)&&b(n)&&(Object.assign(n.style,e),Object.keys(i).forEach(function(t){var e=i[t];!1===e?n.removeAttribute(t):n.setAttribute(t,!0===e?"":e)}))})},effect:function(t){var n=t.state,o={popper:{position:n.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(n.elements.popper.style,o.popper),n.styles=o,n.elements.arrow&&Object.assign(n.elements.arrow.style,o.arrow),function(){Object.keys(n.elements).forEach(function(t){var e=n.elements[t],i=n.attributes[t]||{},t=Object.keys((n.styles.hasOwnProperty(t)?n.styles:o)[t]).reduce(function(t,e){return t[e]="",t},{});_(e)&&b(e)&&(Object.assign(e.style,t),Object.keys(i).forEach(function(t){e.removeAttribute(t)}))})}},requires:["computeStyles"]};function O(t){return t.split("-")[0]}function Jt(t){t=t.getBoundingClientRect();return{width:t.width,height:t.height,top:t.top,right:t.right,bottom:t.bottom,left:t.left,x:t.left,y:t.top}}function Kt(t){var e=Jt(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function te(t,e){var i=e.getRootNode&&e.getRootNode();if(t.contains(e))return!0;if(i&&Zt(i)){var n=e;do{if(n&&t.isSameNode(n))return!0}while(n=n.parentNode||n.host)}return!1}function k(t){return w(t).getComputedStyle(t)}function x(t){return((Xt(t)?t.ownerDocument:t.document)||window.document).documentElement}function ee(t){return"html"===b(t)?t:t.assignedSlot||t.parentNode||(Zt(t)?t.host:null)||x(t)}function ie(t){return _(t)&&"fixed"!==k(t).position?t.offsetParent:null}function ne(t){for(var e,i=w(t),n=ie(t);n&&(e=n,0<=["table","td","th"].indexOf(b(e)))&&"static"===k(n).position;)n=ie(n);return(!n||"html"!==b(n)&&("body"!==b(n)||"static"!==k(n).position))&&(n||function(t){for(var e=-1!==navigator.userAgent.toLowerCase().indexOf("firefox"),i=ee(t);_(i)&&["html","body"].indexOf(b(i))<0;){var n=k(i);if("none"!==n.transform||"none"!==n.perspective||"paint"===n.contain||-1!==["transform","perspective"].indexOf(n.willChange)||e&&"filter"===n.willChange||e&&n.filter&&"none"!==n.filter)return i;i=i.parentNode}return null}(t))||i}function oe(t){return 0<=["top","bottom"].indexOf(t)?"x":"y"}var T=Math.max,se=Math.min,re=Math.round;function ae(t,e,i){return T(t,se(e,i))}function le(){return{top:0,right:0,bottom:0,left:0}}function ce(t){return Object.assign({},le(),t)}function de(i,t){return t.reduce(function(t,e){return t[e]=i,t},{})}var ue={name:"arrow",enabled:!0,phase:"main",fn:function(t){var e,i,n,o,s=t.state,r=t.name,t=t.options,a=s.elements.arrow,l=s.modifiersData.popperOffsets,c=oe(d=O(s.placement)),d=0<=[L,A].indexOf(d)?"height":"width";a&&l&&(t=t.padding,i=s,i=ce("number"!=typeof(t="function"==typeof t?t(Object.assign({},i.rects,{placement:i.placement})):t)?t:de(t,Mt)),t=Kt(a),o="y"===c?E:L,n="y"===c?D:A,e=s.rects.reference[d]+s.rects.reference[c]-l[c]-s.rects.popper[d],l=l[c]-s.rects.reference[c],a=(a=ne(a))?"y"===c?a.clientHeight||0:a.clientWidth||0:0,o=i[o],i=a-t[d]-i[n],o=ae(o,n=a/2-t[d]/2+(e/2-l/2),i),s.modifiersData[r]=((a={})[c]=o,a.centerOffset=o-n,a))},effect:function(t){var e=t.state;null!=(t=void 0===(t=t.options.element)?"[data-popper-arrow]":t)&&("string"!=typeof t||(t=e.elements.popper.querySelector(t)))&&te(e.elements.popper,t)&&(e.elements.arrow=t)},requires:["popperOffsets"],requiresIfExists:["preventOverflow"]},he={top:"auto",right:"auto",bottom:"auto",left:"auto"};function pe(t){var e,i,n,o=t.popper,s=t.popperRect,r=t.placement,a=t.offsets,l=t.position,c=t.gpuAcceleration,d=t.adaptive,t=t.roundOffsets,u=!0===t?(u=(h=a).x,h=a.y,p=window.devicePixelRatio||1,{x:re(re(u*p)/p)||0,y:re(re(h*p)/p)||0}):"function"==typeof t?t(a):a,h=u.x,p=void 0===h?0:h,t=u.y,t=void 0===t?0:t,f=a.hasOwnProperty("x"),a=a.hasOwnProperty("y"),m=L,g=E,y=window,o=(d&&(n="clientHeight",i="clientWidth",(e=ne(o))===w(o)&&"static"!==k(e=x(o)).position&&(n="scrollHeight",i="scrollWidth"),r===E&&(g=D,t=(t-(e[n]-s.height))*(c?1:-1)),r===L)&&(m=A,p=(p-(e[i]-s.width))*(c?1:-1)),Object.assign({position:l},d&&he));return c?Object.assign({},o,((n={})[g]=a?"0":"",n[m]=f?"0":"",n.transform=(y.devicePixelRatio||1)<2?"translate("+p+"px, "+t+"px)":"translate3d("+p+"px, "+t+"px, 0)",n)):Object.assign({},o,((r={})[g]=a?t+"px":"",r[m]=f?p+"px":"",r.transform="",r))}var fe={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:function(t){var e=t.state,t=t.options,i=void 0===(i=t.gpuAcceleration)||i,n=void 0===(n=t.adaptive)||n,t=void 0===(t=t.roundOffsets)||t,i={placement:O(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:i};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,pe(Object.assign({},i,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:n,roundOffsets:t})))),null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,pe(Object.assign({},i,{offsets:e.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:t})))),e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-placement":e.placement})},data:{}},me={passive:!0};var ge={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=(t=t.options).scroll,o=void 0===n||n,s=void 0===(n=t.resize)||n,r=w(e.elements.popper),a=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&a.forEach(function(t){t.addEventListener("scroll",i.update,me)}),s&&r.addEventListener("resize",i.update,me),function(){o&&a.forEach(function(t){t.removeEventListener("scroll",i.update,me)}),s&&r.removeEventListener("resize",i.update,me)}},data:{}},ye={left:"right",right:"left",bottom:"top",top:"bottom"};function ve(t){return t.replace(/left|right|bottom|top/g,function(t){return ye[t]})}var be={start:"end",end:"start"};function we(t){return t.replace(/start|end/g,function(t){return be[t]})}function _e(t){t=w(t);return{scrollLeft:t.pageXOffset,scrollTop:t.pageYOffset}}function ke(t){return Jt(x(t)).left+_e(t).scrollLeft}function xe(t){var t=k(t),e=t.overflow,i=t.overflowX,t=t.overflowY;return/auto|scroll|overlay|hidden/.test(e+t+i)}function Te(t,e){void 0===e&&(e=[]);var i=function t(e){return 0<=["html","body","#document"].indexOf(b(e))?e.ownerDocument.body:_(e)&&xe(e)?e:t(ee(e))}(t),t=i===(null==(t=t.ownerDocument)?void 0:t.body),n=w(i),n=t?[n].concat(n.visualViewport||[],xe(i)?i:[]):i,i=e.concat(n);return t?i:i.concat(Te(ee(n)))}function Se(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function Ce(t,e){return e===It?Se((n=w(i=t),o=x(i),n=n.visualViewport,s=o.clientWidth,o=o.clientHeight,a=r=0,n&&(s=n.width,o=n.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(r=n.offsetLeft,a=n.offsetTop)),{width:s,height:o,x:r+ke(i),y:a})):_(e)?((s=Jt(n=e)).top=s.top+n.clientTop,s.left=s.left+n.clientLeft,s.bottom=s.top+n.clientHeight,s.right=s.left+n.clientWidth,s.width=n.clientWidth,s.height=n.clientHeight,s.x=s.left,s.y=s.top,s):Se((o=x(t),r=x(o),i=_e(o),a=null==(a=o.ownerDocument)?void 0:a.body,e=T(r.scrollWidth,r.clientWidth,a?a.scrollWidth:0,a?a.clientWidth:0),t=T(r.scrollHeight,r.clientHeight,a?a.scrollHeight:0,a?a.clientHeight:0),o=-i.scrollLeft+ke(o),i=-i.scrollTop,"rtl"===k(a||r).direction&&(o+=T(r.clientWidth,a?a.clientWidth:0)-e),{width:e,height:t,x:o,y:i}));var i,n,o,s,r,a}function Ee(i,t,e){var n,o="clippingParents"===t?(s=Te(ee(o=i)),Xt(n=0<=["absolute","fixed"].indexOf(k(o).position)&&_(o)?ne(o):o)?s.filter(function(t){return Xt(t)&&te(t,n)&&"body"!==b(t)}):[]):[].concat(t),s=[].concat(o,[e]),t=s[0],e=s.reduce(function(t,e){e=Ce(i,e);return t.top=T(e.top,t.top),t.right=se(e.right,t.right),t.bottom=se(e.bottom,t.bottom),t.left=T(e.left,t.left),t},Ce(i,t));return e.width=e.right-e.left,e.height=e.bottom-e.top,e.x=e.left,e.y=e.top,e}function De(t){return t.split("-")[1]}function Ae(t){var e,i=t.reference,n=t.element,t=t.placement,o=t?O(t):null,t=t?De(t):null,s=i.x+i.width/2-n.width/2,r=i.y+i.height/2-n.height/2;switch(o){case E:e={x:s,y:i.y-n.height};break;case D:e={x:s,y:i.y+i.height};break;case A:e={x:i.x+i.width,y:r};break;case L:e={x:i.x-n.width,y:r};break;default:e={x:i.x,y:i.y}}var a=o?oe(o):null;if(null!=a){var l="y"===a?"height":"width";switch(t){case jt:e[a]=e[a]-(i[l]/2-n[l]/2);break;case Pt:e[a]=e[a]+(i[l]/2-n[l]/2)}}return e}function Le(t,e){var n,e=e=void 0===e?{}:e,i=e.placement,i=void 0===i?t.placement:i,o=e.boundary,o=void 0===o?Nt:o,s=e.rootBoundary,s=void 0===s?It:s,r=e.elementContext,r=void 0===r?Ht:r,a=e.altBoundary,a=void 0!==a&&a,e=e.padding,e=void 0===e?0:e,e=ce("number"!=typeof e?e:de(e,Mt)),l=t.elements.reference,c=t.rects.popper,a=t.elements[a?r===Ht?Ft:Ht:r],a=Ee(Xt(a)?a:a.contextElement||x(t.elements.popper),o,s),o=Jt(l),s=Ae({reference:o,element:c,strategy:"absolute",placement:i}),l=Se(Object.assign({},c,s)),c=r===Ht?l:o,d={top:a.top-c.top+e.top,bottom:c.bottom-a.bottom+e.bottom,left:a.left-c.left+e.left,right:c.right-a.right+e.right},s=t.modifiersData.offset;return r===Ht&&s&&(n=s[i],Object.keys(d).forEach(function(t){var e=0<=[A,D].indexOf(t)?1:-1,i=0<=[E,D].indexOf(t)?"y":"x";d[t]+=n[i]*e})),d}var Oe={name:"flip",enabled:!0,phase:"main",fn:function(t){var u=t.state,e=t.options,t=t.name;if(!u.modifiersData[t]._skip){for(var i=e.mainAxis,n=void 0===i||i,i=e.altAxis,o=void 0===i||i,i=e.fallbackPlacements,h=e.padding,p=e.boundary,f=e.rootBoundary,s=e.altBoundary,r=e.flipVariations,m=void 0===r||r,g=e.allowedAutoPlacements,r=u.options.placement,e=O(r),i=i||(e===r||!m?[ve(r)]:O(i=r)===$t?[]:(e=ve(i),[we(i),e,we(e)])),a=[r].concat(i).reduce(function(t,e){return t.concat(O(e)===$t?(i=u,n=(t=t=void 0===(t={placement:e,boundary:p,rootBoundary:f,padding:h,flipVariations:m,allowedAutoPlacements:g})?{}:t).placement,o=t.boundary,s=t.rootBoundary,r=t.padding,a=t.flipVariations,l=void 0===(t=t.allowedAutoPlacements)?qt:t,c=De(n),t=c?a?Rt:Rt.filter(function(t){return De(t)===c}):Mt,d=(n=0===(n=t.filter(function(t){return 0<=l.indexOf(t)})).length?t:n).reduce(function(t,e){return t[e]=Le(i,{placement:e,boundary:o,rootBoundary:s,padding:r})[O(e)],t},{}),Object.keys(d).sort(function(t,e){return d[t]-d[e]})):e);var i,n,o,s,r,a,l,c,d},[]),l=u.rects.reference,c=u.rects.popper,d=new Map,y=!0,v=a[0],b=0;b<a.length;b++){var w=a[b],_=O(w),k=De(w)===jt,x=0<=[E,D].indexOf(_),T=x?"width":"height",S=Le(u,{placement:w,boundary:p,rootBoundary:f,altBoundary:s,padding:h}),x=x?k?A:L:k?D:E,k=(l[T]>c[T]&&(x=ve(x)),ve(x)),T=[];if(n&&T.push(S[_]<=0),o&&T.push(S[x]<=0,S[k]<=0),T.every(function(t){return t})){v=w,y=!1;break}d.set(w,T)}if(y)for(var C=m?3:1;0<C;C--)if("break"===function(e){var t=a.find(function(t){t=d.get(t);if(t)return t.slice(0,e).every(function(t){return t})});if(t)return v=t,"break"}(C))break;u.placement!==v&&(u.modifiersData[t]._skip=!0,u.placement=v,u.reset=!0)}},requiresIfExists:["offset"],data:{_skip:!1}};function $e(t,e,i){return{top:t.top-e.height-(i=void 0===i?{x:0,y:0}:i).y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function Me(e){return[E,A,D,L].some(function(t){return 0<=e[t]})}var je={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:function(t){var e=t.state,t=t.name,i=e.rects.reference,n=e.rects.popper,o=e.modifiersData.preventOverflow,s=Le(e,{elementContext:"reference"}),r=Le(e,{altBoundary:!0}),s=$e(s,i),i=$e(r,n,o),r=Me(s),n=Me(i);e.modifiersData[t]={referenceClippingOffsets:s,popperEscapeOffsets:i,isReferenceHidden:r,hasPopperEscaped:n},e.attributes.popper=Object.assign({},e.attributes.popper,{"data-popper-reference-hidden":r,"data-popper-escaped":n})}};var Pe={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:function(t){var r=t.state,e=t.options,t=t.name,a=void 0===(e=e.offset)?[0,0]:e,e=qt.reduce(function(t,e){var i,n,o,s;return t[e]=(e=e,i=r.rects,n=a,o=O(e),s=0<=[L,E].indexOf(o)?-1:1,e=(i="function"==typeof n?n(Object.assign({},i,{placement:e})):n)[0]||0,n=(i[1]||0)*s,0<=[L,A].indexOf(o)?{x:n,y:e}:{x:e,y:n}),t},{}),i=(n=e[r.placement]).x,n=n.y;null!=r.modifiersData.popperOffsets&&(r.modifiersData.popperOffsets.x+=i,r.modifiersData.popperOffsets.y+=n),r.modifiersData[t]=e}};var Ne={name:"popperOffsets",enabled:!0,phase:"read",fn:function(t){var e=t.state,t=t.name;e.modifiersData[t]=Ae({reference:e.rects.reference,element:e.rects.popper,strategy:"absolute",placement:e.placement})},data:{}};var Ie={name:"preventOverflow",enabled:!0,phase:"main",fn:function(t){var e,i,n,o,s,r,a,l,c,d=t.state,u=t.options,t=t.name,h=void 0===(h=u.mainAxis)||h,p=void 0!==(p=u.altAxis)&&p,f=u.boundary,m=u.rootBoundary,g=u.altBoundary,y=u.padding,v=void 0===(v=u.tether)||v,u=void 0===(u=u.tetherOffset)?0:u,f=Le(d,{boundary:f,rootBoundary:m,padding:y,altBoundary:g}),m=O(d.placement),g=!(y=De(d.placement)),b="x"===(m=oe(m))?"y":"x",w=d.modifiersData.popperOffsets,_=d.rects.reference,k=d.rects.popper,u="function"==typeof u?u(Object.assign({},d.rects,{placement:d.placement})):u,x={x:0,y:0};w&&((h||p)&&(s="y"===m?"height":"width",e=w[m],i=w[m]+f[c="y"===m?E:L],n=w[m]-f[a="y"===m?D:A],r=v?-k[s]/2:0,o=(y===jt?_:k)[s],y=y===jt?-k[s]:-_[s],k=d.elements.arrow,k=v&&k?Kt(k):{width:0,height:0},c=(l=d.modifiersData["arrow#persistent"]?d.modifiersData["arrow#persistent"].padding:le())[c],l=l[a],a=ae(0,_[s],k[s]),k=g?_[s]/2-r-a-c-u:o-a-c-u,o=g?-_[s]/2+r+a+l+u:y+a+l+u,g=(c=d.elements.arrow&&ne(d.elements.arrow))?"y"===m?c.clientTop||0:c.clientLeft||0:0,_=d.modifiersData.offset?d.modifiersData.offset[d.placement][m]:0,s=w[m]+k-_-g,r=w[m]+o-_,h&&(y=ae(v?se(i,s):i,e,v?T(n,r):n),w[m]=y,x[m]=y-e),p)&&(l=(a=w[b])+f["x"===m?E:L],u=a-f["x"===m?D:A],c=ae(v?se(l,s):l,a,v?T(u,r):u),w[b]=c,x[b]=c-a),d.modifiersData[t]=x)},requiresIfExists:["offset"]};function He(t,e,i){void 0===i&&(i=!1);var n=x(e),t=Jt(t),o=_(e),s={scrollLeft:0,scrollTop:0},r={x:0,y:0};return!o&&i||("body"===b(e)&&!xe(n)||(s=(o=e)!==w(o)&&_(o)?{scrollLeft:o.scrollLeft,scrollTop:o.scrollTop}:_e(o)),_(e)?((r=Jt(e)).x+=e.clientLeft,r.y+=e.clientTop):n&&(r.x=ke(n))),{x:t.left+s.scrollLeft-r.x,y:t.top+s.scrollTop-r.y,width:t.width,height:t.height}}function Fe(t){var i=new Map,n=new Set,o=[];return t.forEach(function(t){i.set(t.name,t)}),t.forEach(function(t){n.has(t.name)||!function e(t){n.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach(function(t){n.has(t)||(t=i.get(t))&&e(t)}),o.push(t)}(t)}),o}var Re={placement:"bottom",modifiers:[],strategy:"absolute"};function qe(){for(var t=arguments.length,e=new Array(t),i=0;i<t;i++)e[i]=arguments[i];return!e.some(function(t){return!(t&&"function"==typeof t.getBoundingClientRect)})}function Ye(t){var t=t=void 0===t?{}:t,e=t.defaultModifiers,u=void 0===e?[]:e,e=t.defaultOptions,h=void 0===e?Re:e;return function(n,o,e){void 0===e&&(e=h);var i,s,r={placement:"bottom",orderedModifiers:[],options:Object.assign({},Re,h),modifiersData:{},elements:{reference:n,popper:o},attributes:{},styles:{}},a=[],l=!1,c={state:r,setOptions:function(t){d(),r.options=Object.assign({},h,r.options,t),r.scrollParents={reference:Xt(n)?Te(n):n.contextElement?Te(n.contextElement):[],popper:Te(o)};t=[].concat(u,r.options.modifiers),e=t.reduce(function(t,e){var i=t[e.name];return t[e.name]=i?Object.assign({},i,e,{options:Object.assign({},i.options,e.options),data:Object.assign({},i.data,e.data)}):e,t},{}),t=Object.keys(e).map(function(t){return e[t]}),i=Fe(t);var i,e,t=Vt.reduce(function(t,e){return t.concat(i.filter(function(t){return t.phase===e}))},[]);return r.orderedModifiers=t.filter(function(t){return t.enabled}),r.orderedModifiers.forEach(function(t){var e=t.name,i=t.options,t=t.effect;"function"==typeof t&&(t=t({state:r,name:e,instance:c,options:void 0===i?{}:i}),a.push(t||function(){}))}),c.update()},forceUpdate:function(){if(!l){var t=r.elements,e=t.reference,t=t.popper;if(qe(e,t)){r.rects={reference:He(e,ne(t),"fixed"===r.options.strategy),popper:Kt(t)},r.reset=!1,r.placement=r.options.placement,r.orderedModifiers.forEach(function(t){return r.modifiersData[t.name]=Object.assign({},t.data)});for(var i,n,o,s=0;s<r.orderedModifiers.length;s++)!0===r.reset?(r.reset=!1,s=-1):(i=(o=r.orderedModifiers[s]).fn,n=o.options,o=o.name,"function"==typeof i&&(r=i({state:r,options:void 0===n?{}:n,name:o,instance:c})||r))}}},update:(i=function(){return new Promise(function(t){c.forceUpdate(),t(r)})},function(){return s=s||new Promise(function(t){Promise.resolve().then(function(){s=void 0,t(i())})})}),destroy:function(){d(),l=!0}};return qe(n,o)&&c.setOptions(e).then(function(t){!l&&e.onFirstUpdate&&e.onFirstUpdate(t)}),c;function d(){a.forEach(function(t){return t()}),a=[]}}}var We=Ye({defaultModifiers:[ge,Ne,fe,Qt,Pe,Oe,Ie,ue,je]}),Be=Object.freeze({__proto__:null,popperGenerator:Ye,detectOverflow:Le,createPopperBase:Ye(),createPopper:We,createPopperLite:Ye({defaultModifiers:[ge,Ne,fe,Qt]}),top:E,bottom:D,right:A,left:L,auto:$t,basePlacements:Mt,start:jt,end:Pt,clippingParents:Nt,viewport:It,popper:Ht,reference:Ft,variationPlacements:Rt,placements:qt,beforeRead:Yt,read:"read",afterRead:Wt,beforeMain:Bt,main:"main",afterMain:zt,beforeWrite:Ut,write:"write",afterWrite:Gt,modifierPhases:Vt,applyStyles:Qt,arrow:ue,computeStyles:fe,eventListeners:ge,flip:Oe,hide:je,offset:Pe,popperOffsets:Ne,preventOverflow:Ie});const ze="dropdown",Ue="bs.dropdown",S="."+Ue;Yt=".data-api";const Ge="Escape",Ve="ArrowUp",Xe="ArrowDown",Ze=new RegExp(Ve+`|${Xe}|`+Ge),Qe="hide"+S,Je="hidden"+S;S,S,S;Wt="click"+S+Yt,Bt="keydown"+S+Yt;S;const Ke="disabled",C="show",ti='[data-bs-toggle="dropdown"]',ei=".dropdown-menu",ii=e()?"top-end":"top-start",ni=e()?"top-start":"top-end",oi=e()?"bottom-end":"bottom-start",si=e()?"bottom-start":"bottom-end",ri=e()?"left-start":"right-start",ai=e()?"right-start":"left-start",li={offset:[0,2],boundary:"clippingParents",reference:"toggle",display:"dynamic",popperConfig:null},ci={offset:"(array|string|function)",boundary:"(string|element)",reference:"(string|element|object)",display:"string",popperConfig:"(null|object|function)"};class $ extends s{constructor(t,e){super(t),this._popper=null,this._config=this._getConfig(e),this._menu=this._getMenuElement(),this._inNavbar=this._detectNavbar(),this._addEventListeners()}static get Default(){return li}static get DefaultType(){return ci}static get DATA_KEY(){return Ue}toggle(){var t;this._element.disabled||this._element.classList.contains(Ke)||(t=this._element.classList.contains(C),$.clearMenus(),t)||this.show()}show(){if(!(this._element.disabled||this._element.classList.contains(Ke)||this._menu.classList.contains(C))){var e=$.getParentFromElement(this._element),t={relatedTarget:this._element},i=m.trigger(this._element,"show.bs.dropdown",t);if(!i.defaultPrevented){if(this._inNavbar)l.setDataAttribute(this._menu,"popper","none");else{if(void 0===Be)throw new TypeError("Bootstrap's dropdowns require Popper (https://popper.js.org)");let t=this._element;"parent"===this._config.reference?t=e:r(this._config.reference)?(t=this._config.reference,void 0!==this._config.reference.jquery&&(t=this._config.reference[0])):"object"==typeof this._config.reference&&(t=this._config.reference);var i=this._getPopperConfig(),n=i.modifiers.find(t=>"applyStyles"===t.name&&!1===t.enabled);this._popper=We(t,this._menu,i),n&&l.setDataAttribute(this._menu,"popper","static")}"ontouchstart"in document.documentElement&&!e.closest(".navbar-nav")&&[].concat(...document.body.children).forEach(t=>m.on(t,"mouseover",null,B())),this._element.focus(),this._element.setAttribute("aria-expanded",!0),this._menu.classList.toggle(C),this._element.classList.toggle(C),m.trigger(this._element,"shown.bs.dropdown",t)}}}hide(){var t;this._element.disabled||this._element.classList.contains(Ke)||!this._menu.classList.contains(C)||(t={relatedTarget:this._element},m.trigger(this._element,Qe,t).defaultPrevented)||(this._popper&&this._popper.destroy(),this._menu.classList.toggle(C),this._element.classList.toggle(C),l.removeDataAttribute(this._menu,"popper"),m.trigger(this._element,Je,t))}dispose(){m.off(this._element,S),this._menu=null,this._popper&&(this._popper.destroy(),this._popper=null),super.dispose()}update(){this._inNavbar=this._detectNavbar(),this._popper&&this._popper.update()}_addEventListeners(){m.on(this._element,"click.bs.dropdown",t=>{t.preventDefault(),this.toggle()})}_getConfig(t){if(t={...this.constructor.Default,...l.getDataAttributes(this._element),...t},i(ze,t,this.constructor.DefaultType),"object"!=typeof t.reference||r(t.reference)||"function"==typeof t.reference.getBoundingClientRect)return t;throw new TypeError(ze.toUpperCase()+': Option "reference" provided type "object" without a required "getBoundingClientRect" method.')}_getMenuElement(){return h.next(this._element,ei)[0]}_getPlacement(){var t,e=this._element.parentNode;return e.classList.contains("dropend")?ri:e.classList.contains("dropstart")?ai:(t="end"===getComputedStyle(this._menu).getPropertyValue("--bs-position").trim(),e.classList.contains("dropup")?t?ni:ii:t?si:oi)}_detectNavbar(){return null!==this._element.closest(".navbar")}_getOffset(){const e=this._config["offset"];return"string"==typeof e?e.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof e?t=>e(t,this._element):e}_getPopperConfig(){var t={placement:this._getPlacement(),modifiers:[{name:"preventOverflow",options:{boundary:this._config.boundary}},{name:"offset",options:{offset:this._getOffset()}}]};return"static"===this._config.display&&(t.modifiers=[{name:"applyStyles",enabled:!1}]),{...t,..."function"==typeof this._config.popperConfig?this._config.popperConfig(t):this._config.popperConfig}}static dropdownInterface(t,e){let i=a(t,Ue);var n="object"==typeof e?e:null;if(i=i||new $(t,n),"string"==typeof e){if(void 0===i[e])throw new TypeError(`No method named "${e}"`);i[e]()}}static jQueryInterface(t){return this.each(function(){$.dropdownInterface(this,t)})}static clearMenus(i){if(i){if(2===i.button||"keyup"===i.type&&"Tab"!==i.key)return;if(/input|select|textarea|form/i.test(i.target.tagName))return}var n=h.find(ti);for(let t=0,e=n.length;t<e;t++){var o=a(n[t],Ue),s={relatedTarget:n[t]};if(i&&"click"===i.type&&(s.clickEvent=i),o){var r=o._menu;if(n[t].classList.contains(C)){if(i){if([o._element].some(t=>i.composedPath().includes(t)))continue;if("keyup"===i.type&&"Tab"===i.key&&r.contains(i.target))continue}m.trigger(n[t],Qe,s).defaultPrevented||("ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>m.off(t,"mouseover",null,B())),n[t].setAttribute("aria-expanded","false"),o._popper&&o._popper.destroy(),r.classList.remove(C),n[t].classList.remove(C),l.removeDataAttribute(r,"popper"),m.trigger(n[t],Je,s))}}}}static getParentFromElement(t){return o(t)||t.parentNode}static dataApiKeydownHandler(e){if((/input|textarea/i.test(e.target.tagName)?!("Space"===e.key||e.key!==Ge&&(e.key!==Xe&&e.key!==Ve||e.target.closest(ei))):Ze.test(e.key))&&(e.preventDefault(),e.stopPropagation(),!this.disabled)&&!this.classList.contains(Ke)){var t=$.getParentFromElement(this),i=this.classList.contains(C);if(e.key===Ge)(this.matches(ti)?this:h.prev(this,ti)[0]).focus(),$.clearMenus();else if(i||e.key!==Ve&&e.key!==Xe)if(i&&"Space"!==e.key){i=h.find(".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)",t).filter(q);if(i.length){let t=i.indexOf(e.target);e.key===Ve&&0<t&&t--,e.key===Xe&&t<i.length-1&&t++,i[t=-1===t?0:t].focus()}}else $.clearMenus();else(this.matches(ti)?this:h.prev(this,ti)[0]).click()}}}m.on(document,Bt,ti,$.dataApiKeydownHandler),m.on(document,Bt,ei,$.dataApiKeydownHandler),m.on(document,Wt,$.clearMenus),m.on(document,"keyup.bs.dropdown.data-api",$.clearMenus),m.on(document,Wt,ti,function(t){t.preventDefault(),$.dropdownInterface(this)}),t(ze,$);const di="bs.modal",M="."+di;const ui={backdrop:!0,keyboard:!0,focus:!0},hi={backdrop:"(boolean|string)",keyboard:"boolean",focus:"boolean"},pi=(M,M,"hidden"+M),fi="show"+M,mi=(M,"focusin"+M),gi="resize"+M,yi="click.dismiss"+M,vi="keydown.dismiss"+M,bi=(M,"mousedown.dismiss"+M);M;const wi="modal-open",_i="show",ki="modal-static";const xi=".fixed-top, .fixed-bottom, .is-fixed, .sticky-top",Ti=".sticky-top";class Si extends s{constructor(t,e){super(t),this._config=this._getConfig(e),this._dialog=h.findOne(".modal-dialog",this._element),this._backdrop=null,this._isShown=!1,this._isBodyOverflowing=!1,this._ignoreBackdropClick=!1,this._isTransitioning=!1,this._scrollbarWidth=0}static get Default(){return ui}static get DATA_KEY(){return di}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){var e;this._isShown||this._isTransitioning||(this._isAnimated()&&(this._isTransitioning=!0),e=m.trigger(this._element,fi,{relatedTarget:t}),this._isShown)||e.defaultPrevented||(this._isShown=!0,this._checkScrollbar(),this._setScrollbar(),this._adjustDialog(),this._setEscapeEvent(),this._setResizeEvent(),m.on(this._element,yi,'[data-bs-dismiss="modal"]',t=>this.hide(t)),m.on(this._dialog,bi,()=>{m.one(this._element,"mouseup.dismiss.bs.modal",t=>{t.target===this._element&&(this._ignoreBackdropClick=!0)})}),this._showBackdrop(()=>this._showElement(t)))}hide(t){t&&t.preventDefault(),!this._isShown||this._isTransitioning||m.trigger(this._element,"hide.bs.modal").defaultPrevented||(this._isShown=!1,(t=this._isAnimated())&&(this._isTransitioning=!0),this._setEscapeEvent(),this._setResizeEvent(),m.off(document,mi),this._element.classList.remove(_i),m.off(this._element,yi),m.off(this._dialog,bi),t?(t=d(this._element),m.one(this._element,"transitionend",t=>this._hideModal(t)),u(this._element,t)):this._hideModal())}dispose(){[window,this._element,this._dialog].forEach(t=>m.off(t,M)),super.dispose(),m.off(document,mi),this._config=null,this._dialog=null,this._backdrop=null,this._isShown=null,this._isBodyOverflowing=null,this._ignoreBackdropClick=null,this._isTransitioning=null,this._scrollbarWidth=null}handleUpdate(){this._adjustDialog()}_getConfig(t){return t={...ui,...t},i("modal",t,hi),t}_showElement(t){var e=this._isAnimated(),i=h.findOne(".modal-body",this._dialog),i=(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.scrollTop=0,i&&(i.scrollTop=0),e&&z(this._element),this._element.classList.add(_i),this._config.focus&&this._enforceFocus(),()=>{this._config.focus&&this._element.focus(),this._isTransitioning=!1,m.trigger(this._element,"shown.bs.modal",{relatedTarget:t})});e?(e=d(this._dialog),m.one(this._dialog,"transitionend",i),u(this._dialog,e)):i()}_enforceFocus(){m.off(document,mi),m.on(document,mi,t=>{document===t.target||this._element===t.target||this._element.contains(t.target)||this._element.focus()})}_setEscapeEvent(){this._isShown?m.on(this._element,vi,t=>{this._config.keyboard&&"Escape"===t.key?(t.preventDefault(),this.hide()):this._config.keyboard||"Escape"!==t.key||this._triggerBackdropTransition()}):m.off(this._element,vi)}_setResizeEvent(){this._isShown?m.on(window,gi,()=>this._adjustDialog()):m.off(window,gi)}_hideModal(){this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._showBackdrop(()=>{document.body.classList.remove(wi),this._resetAdjustments(),this._resetScrollbar(),m.trigger(this._element,pi)})}_removeBackdrop(){this._backdrop.parentNode.removeChild(this._backdrop),this._backdrop=null}_showBackdrop(t){var e,i=this._isAnimated();this._isShown&&this._config.backdrop?(this._backdrop=document.createElement("div"),this._backdrop.className="modal-backdrop",i&&this._backdrop.classList.add("fade"),document.body.appendChild(this._backdrop),m.on(this._element,yi,t=>{this._ignoreBackdropClick?this._ignoreBackdropClick=!1:t.target===t.currentTarget&&("static"===this._config.backdrop?this._triggerBackdropTransition():this.hide())}),i&&z(this._backdrop),this._backdrop.classList.add(_i),i?(e=d(this._backdrop),m.one(this._backdrop,"transitionend",t),u(this._backdrop,e)):t()):!this._isShown&&this._backdrop?(this._backdrop.classList.remove(_i),e=()=>{this._removeBackdrop(),t()},i?(i=d(this._backdrop),m.one(this._backdrop,"transitionend",e),u(this._backdrop,i)):e()):t()}_isAnimated(){return this._element.classList.contains("fade")}_triggerBackdropTransition(){var t=m.trigger(this._element,"hidePrevented.bs.modal");if(!t.defaultPrevented){const e=this._element.scrollHeight>document.documentElement.clientHeight,i=(e||(this._element.style.overflowY="hidden"),this._element.classList.add(ki),d(this._dialog));m.off(this._element,"transitionend"),m.one(this._element,"transitionend",()=>{this._element.classList.remove(ki),e||(m.one(this._element,"transitionend",()=>{this._element.style.overflowY=""}),u(this._element,i))}),u(this._element,i),this._element.focus()}}_adjustDialog(){var t=this._element.scrollHeight>document.documentElement.clientHeight;(!this._isBodyOverflowing&&t&&!e()||this._isBodyOverflowing&&!t&&e())&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),(this._isBodyOverflowing&&!t&&!e()||!this._isBodyOverflowing&&t&&e())&&(this._element.style.paddingRight=this._scrollbarWidth+"px")}_resetAdjustments(){this._element.style.paddingLeft="",this._element.style.paddingRight=""}_checkScrollbar(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=Math.round(t.left+t.right)<window.innerWidth,this._scrollbarWidth=this._getScrollbarWidth()}_setScrollbar(){this._isBodyOverflowing&&(this._setElementAttributes(xi,"paddingRight",t=>t+this._scrollbarWidth),this._setElementAttributes(Ti,"marginRight",t=>t-this._scrollbarWidth),this._setElementAttributes("body","paddingRight",t=>t+this._scrollbarWidth)),document.body.classList.add(wi)}_setElementAttributes(t,n,o){h.find(t).forEach(t=>{var e,i;t!==document.body&&window.innerWidth>t.clientWidth+this._scrollbarWidth||(e=t.style[n],i=window.getComputedStyle(t)[n],l.setDataAttribute(t,n,e),t.style[n]=o(Number.parseFloat(i))+"px")})}_resetScrollbar(){this._resetElementAttributes(xi,"paddingRight"),this._resetElementAttributes(Ti,"marginRight"),this._resetElementAttributes("body","paddingRight")}_resetElementAttributes(t,i){h.find(t).forEach(t=>{var e=l.getDataAttribute(t,i);void 0===e&&t===document.body?t.style[i]="":(l.removeDataAttribute(t,i),t.style[i]=e)})}_getScrollbarWidth(){var t=document.createElement("div"),e=(t.className="modal-scrollbar-measure",document.body.appendChild(t),t.getBoundingClientRect().width-t.clientWidth);return document.body.removeChild(t),e}static jQueryInterface(i,n){return this.each(function(){let t=a(this,di);var e={...ui,...l.getDataAttributes(this),..."object"==typeof i&&i?i:{}};if(t=t||new Si(this,e),"string"==typeof i){if(void 0===t[i])throw new TypeError(`No method named "${i}"`);t[i](n)}})}}m.on(document,"click.bs.modal.data-api",'[data-bs-toggle="modal"]',function(t){const e=o(this);"A"!==this.tagName&&"AREA"!==this.tagName||t.preventDefault(),m.one(e,fi,t=>{t.defaultPrevented||m.one(e,pi,()=>{q(this)&&this.focus()})});let i=a(e,di);i||(t={...l.getDataAttributes(e),...l.getDataAttributes(this)},i=new Si(e,t)),i.toggle(this)}),t("modal",Si);const Ci=".fixed-top, .fixed-bottom, .is-fixed",Ei=".sticky-top",Di=()=>{var t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)},Ai=(t,n,o)=>{const s=Di();h.find(t).forEach(t=>{var e,i;t!==document.body&&window.innerWidth>t.clientWidth+s||(e=t.style[n],i=window.getComputedStyle(t)[n],l.setDataAttribute(t,n,e),t.style[n]=o(Number.parseFloat(i))+"px")})},Li=(t,i)=>{h.find(t).forEach(t=>{var e=l.getDataAttribute(t,i);void 0===e&&t===document.body?t.style.removeProperty(i):(l.removeDataAttribute(t,i),t.style[i]=e)})},Oi="offcanvas",$i="bs.offcanvas";zt="."+$i,Ut=".data-api";const Mi={backdrop:!0,keyboard:!0,scroll:!1},ji={backdrop:"boolean",keyboard:"boolean",scroll:"boolean"},Pi="offcanvas-backdrop",Ni="offcanvas-toggling",Ii=".offcanvas.show",Hi=(Ii,Ni,"hidden"+zt),Fi="focusin"+zt,Ri="click"+zt+Ut;class qi extends s{constructor(t,e){super(t),this._config=this._getConfig(e),this._isShown=!1,this._addEventListeners()}static get Default(){return Mi}static get DATA_KEY(){return $i}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){var e;this._isShown||m.trigger(this._element,"show.bs.offcanvas",{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._element.style.visibility="visible",this._config.backdrop&&document.body.classList.add(Pi),this._config.scroll||(e=Di(),document.body.style.overflow="hidden",Ai(Ci,"paddingRight",t=>t+e),Ai(Ei,"marginRight",t=>t-e),Ai("body","paddingRight",t=>t+e)),this._element.classList.add(Ni),this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),this._element.classList.add("show"),setTimeout(()=>{this._element.classList.remove(Ni),m.trigger(this._element,"shown.bs.offcanvas",{relatedTarget:t}),this._enforceFocusOnElement(this._element)},d(this._element)))}hide(){this._isShown&&!m.trigger(this._element,"hide.bs.offcanvas").defaultPrevented&&(this._element.classList.add(Ni),m.off(document,Fi),this._element.blur(),this._isShown=!1,this._element.classList.remove("show"),setTimeout(()=>{this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._element.style.visibility="hidden",this._config.backdrop&&document.body.classList.remove(Pi),this._config.scroll||(document.body.style.overflow="auto",Li(Ci,"paddingRight"),Li(Ei,"marginRight"),Li("body","paddingRight")),m.trigger(this._element,Hi),this._element.classList.remove(Ni)},d(this._element)))}_getConfig(t){return t={...Mi,...l.getDataAttributes(this._element),..."object"==typeof t?t:{}},i(Oi,t,ji),t}_enforceFocusOnElement(e){m.off(document,Fi),m.on(document,Fi,t=>{document===t.target||e===t.target||e.contains(t.target)||e.focus()}),e.focus()}_addEventListeners(){m.on(this._element,"click.dismiss.bs.offcanvas",'[data-bs-dismiss="offcanvas"]',()=>this.hide()),m.on(document,"keydown",t=>{this._config.keyboard&&"Escape"===t.key&&this.hide()}),m.on(document,Ri,t=>{var e=h.findOne(F(t.target));this._element.contains(t.target)||e===this._element||this.hide()})}static jQueryInterface(e){return this.each(function(){var t=a(this,$i)||new qi(this,"object"==typeof e?e:{});if("string"==typeof e){if(void 0===t[e]||e.startsWith("_")||"constructor"===e)throw new TypeError(`No method named "${e}"`);t[e](this)}})}}m.on(document,Ri,'[data-bs-toggle="offcanvas"]',function(t){var e=o(this);["A","AREA"].includes(this.tagName)&&t.preventDefault(),Y(this)||(m.one(e,Hi,()=>{q(this)&&this.focus()}),(t=h.findOne(".offcanvas.show, .offcanvas-toggling"))&&t!==e)||(a(e,$i)||new qi(e)).toggle(this)}),m.on(window,"load.bs.offcanvas.data-api",()=>{h.find(Ii).forEach(t=>(a(t,$i)||new qi(t)).show())}),t(Oi,qi);const Yi=new Set(["background","cite","href","itemtype","longdesc","poster","src","xlink:href"]);const Wi=/^(?:(?:https?|mailto|ftp|tel|file):|[^#&/:?]*(?:[#/?]|$))/i,Bi=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[\d+/a-z]+=*$/i;Gt={"*":["class","dir","id","lang","role",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","srcset","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]};function zi(t,i,e){if(!t.length)return t;if(e&&"function"==typeof e)return e(t);var e=(new window.DOMParser).parseFromString(t,"text/html"),n=Object.keys(i),o=[].concat(...e.body.querySelectorAll("*"));for(let t=0,e=o.length;t<e;t++){const a=o[t];var s=a.nodeName.toLowerCase();if(n.includes(s)){var r=[].concat(...a.attributes);const l=[].concat(i["*"]||[],i[s]||[]);r.forEach(t=>{((t,e)=>{var i=t.nodeName.toLowerCase();if(e.includes(i))return!Yi.has(i)||Boolean(Wi.test(t.nodeValue)||Bi.test(t.nodeValue));var n=e.filter(t=>t instanceof RegExp);for(let t=0,e=n.length;t<e;t++)if(n[t].test(i))return!0;return!1})(t,l)||a.removeAttribute(t.nodeName)})}else a.parentNode.removeChild(a)}return e.body.innerHTML}const Ui="tooltip",Gi="bs.tooltip",j="."+Gi,Vi="bs-tooltip",Xi=new RegExp(`(^|\\s)${Vi}\\S+`,"g"),Zi=new Set(["sanitize","allowList","sanitizeFn"]),Qi={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(array|string|function)",container:"(string|element|boolean)",fallbackPlacements:"array",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",allowList:"object",popperConfig:"(null|object|function)"},Ji={AUTO:"auto",TOP:"top",RIGHT:e()?"left":"right",BOTTOM:"bottom",LEFT:e()?"right":"left"},Ki={animation:!0,template:'<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:[0,0],container:!1,fallbackPlacements:["top","right","bottom","left"],boundary:"clippingParents",customClass:"",sanitize:!0,sanitizeFn:null,allowList:Gt,popperConfig:null},tn={HIDE:"hide"+j,HIDDEN:"hidden"+j,SHOW:"show"+j,SHOWN:"shown"+j,INSERTED:"inserted"+j,CLICK:"click"+j,FOCUSIN:"focusin"+j,FOCUSOUT:"focusout"+j,MOUSEENTER:"mouseenter"+j,MOUSELEAVE:"mouseleave"+j},en="fade",nn="show",on="show",sn="hover",rn="focus";class an extends s{constructor(t,e){if(void 0===Be)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");super(t),this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.config=this._getConfig(e),this.tip=null,this._setListeners()}static get Default(){return Ki}static get NAME(){return Ui}static get DATA_KEY(){return Gi}static get Event(){return tn}static get EVENT_KEY(){return j}static get DefaultType(){return Qi}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(t){this._isEnabled&&(t?((t=this._initializeOnDelegatedTarget(t))._activeTrigger.click=!t._activeTrigger.click,t._isWithActiveTrigger()?t._enter(null,t):t._leave(null,t)):this.getTipElement().classList.contains(nn)?this._leave(null,this):this._enter(null,this))}dispose(){clearTimeout(this._timeout),m.off(this._element,this.constructor.EVENT_KEY),m.off(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this.tip&&this.tip.parentNode&&this.tip.parentNode.removeChild(this.tip),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.config=null,this.tip=null,super.dispose()}show(){if("none"===this._element.style.display)throw new Error("Please use show on visible elements");var t,e,i;this.isWithContent()&&this._isEnabled&&(i=m.trigger(this._element,this.constructor.Event.SHOW),e=(null===(e=W(this._element))?this._element.ownerDocument.documentElement:e).contains(this._element),!i.defaultPrevented)&&e&&(i=this.getTipElement(),e=I(this.constructor.NAME),i.setAttribute("id",e),this._element.setAttribute("aria-describedby",e),this.setContent(),this.config.animation&&i.classList.add(en),e="function"==typeof this.config.placement?this.config.placement.call(this,i,this._element):this.config.placement,e=this._getAttachment(e),this._addAttachmentClass(e),t=this._getContainer(),G(i,this.constructor.DATA_KEY,this),this._element.ownerDocument.documentElement.contains(this.tip)||(t.appendChild(i),m.trigger(this._element,this.constructor.Event.INSERTED)),this._popper?this._popper.update():this._popper=We(this._element,i,this._getPopperConfig(e)),i.classList.add(nn),(t="function"==typeof this.config.customClass?this.config.customClass():this.config.customClass)&&i.classList.add(...t.split(" ")),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>{m.on(t,"mouseover",B())}),e=()=>{var t=this._hoverState;this._hoverState=null,m.trigger(this._element,this.constructor.Event.SHOWN),"out"===t&&this._leave(null,this)},this.tip.classList.contains(en)?(i=d(this.tip),m.one(this.tip,"transitionend",e),u(this.tip,i)):e())}hide(){if(this._popper){const i=this.getTipElement();var t,e=()=>{this._isWithActiveTrigger()||(this._hoverState!==on&&i.parentNode&&i.parentNode.removeChild(i),this._cleanTipClass(),this._element.removeAttribute("aria-describedby"),m.trigger(this._element,this.constructor.Event.HIDDEN),this._popper&&(this._popper.destroy(),this._popper=null))};m.trigger(this._element,this.constructor.Event.HIDE).defaultPrevented||(i.classList.remove(nn),"ontouchstart"in document.documentElement&&[].concat(...document.body.children).forEach(t=>m.off(t,"mouseover",B)),this._activeTrigger.click=!1,this._activeTrigger[rn]=!1,this._activeTrigger[sn]=!1,this.tip.classList.contains(en)?(t=d(i),m.one(i,"transitionend",e),u(i,t)):e(),this._hoverState="")}}update(){null!==this._popper&&this._popper.update()}isWithContent(){return Boolean(this.getTitle())}getTipElement(){var t;return this.tip||((t=document.createElement("div")).innerHTML=this.config.template,this.tip=t.children[0]),this.tip}setContent(){var t=this.getTipElement();this.setElementContent(h.findOne(".tooltip-inner",t),this.getTitle()),t.classList.remove(en,nn)}setElementContent(t,e){null!==t&&("object"==typeof e&&r(e)?(e.jquery&&(e=e[0]),this.config.html?e.parentNode!==t&&(t.innerHTML="",t.appendChild(e)):t.textContent=e.textContent):this.config.html?(this.config.sanitize&&(e=zi(e,this.config.allowList,this.config.sanitizeFn)),t.innerHTML=e):t.textContent=e)}getTitle(){let t=this._element.getAttribute("data-bs-original-title");return t=t||("function"==typeof this.config.title?this.config.title.call(this._element):this.config.title)}updateAttachment(t){return"right"===t?"end":"left"===t?"start":t}_initializeOnDelegatedTarget(t,e){var i=this.constructor.DATA_KEY;return(e=e||a(t.delegateTarget,i))||(e=new this.constructor(t.delegateTarget,this._getDelegateConfig()),G(t.delegateTarget,i,e)),e}_getOffset(){const e=this.config["offset"];return"string"==typeof e?e.split(",").map(t=>Number.parseInt(t,10)):"function"==typeof e?t=>e(t,this._element):e}_getPopperConfig(t){t={placement:t,modifiers:[{name:"flip",options:{altBoundary:!0,fallbackPlacements:this.config.fallbackPlacements}},{name:"offset",options:{offset:this._getOffset()}},{name:"preventOverflow",options:{boundary:this.config.boundary}},{name:"arrow",options:{element:`.${this.constructor.NAME}-arrow`}},{name:"onChange",enabled:!0,phase:"afterWrite",fn:t=>this._handlePopperPlacementChange(t)}],onFirstUpdate:t=>{t.options.placement!==t.placement&&this._handlePopperPlacementChange(t)}};return{...t,..."function"==typeof this.config.popperConfig?this.config.popperConfig(t):this.config.popperConfig}}_addAttachmentClass(t){this.getTipElement().classList.add(Vi+"-"+this.updateAttachment(t))}_getContainer(){return!1===this.config.container?document.body:r(this.config.container)?this.config.container:h.findOne(this.config.container)}_getAttachment(t){return Ji[t.toUpperCase()]}_setListeners(){this.config.trigger.split(" ").forEach(t=>{var e;"click"===t?m.on(this._element,this.constructor.Event.CLICK,this.config.selector,t=>this.toggle(t)):"manual"!==t&&(e=t===sn?this.constructor.Event.MOUSEENTER:this.constructor.Event.FOCUSIN,t=t===sn?this.constructor.Event.MOUSELEAVE:this.constructor.Event.FOCUSOUT,m.on(this._element,e,this.config.selector,t=>this._enter(t)),m.on(this._element,t,this.config.selector,t=>this._leave(t)))}),this._hideModalHandler=()=>{this._element&&this.hide()},m.on(this._element.closest(".modal"),"hide.bs.modal",this._hideModalHandler),this.config.selector?this.config={...this.config,trigger:"manual",selector:""}:this._fixTitle()}_fixTitle(){var t=this._element.getAttribute("title"),e=typeof this._element.getAttribute("data-bs-original-title");!t&&"string"==e||(this._element.setAttribute("data-bs-original-title",t||""),!t||this._element.getAttribute("aria-label")||this._element.textContent||this._element.setAttribute("aria-label",t),this._element.setAttribute("title",""))}_enter(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusin"===t.type?rn:sn]=!0),e.getTipElement().classList.contains(nn)||e._hoverState===on?e._hoverState=on:(clearTimeout(e._timeout),e._hoverState=on,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(()=>{e._hoverState===on&&e.show()},e.config.delay.show):e.show())}_leave(t,e){e=this._initializeOnDelegatedTarget(t,e),t&&(e._activeTrigger["focusout"===t.type?rn:sn]=e._element.contains(t.relatedTarget)),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState="out",e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(()=>{"out"===e._hoverState&&e.hide()},e.config.delay.hide):e.hide())}_isWithActiveTrigger(){for(const t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1}_getConfig(t){const e=l.getDataAttributes(this._element);return Object.keys(e).forEach(t=>{Zi.has(t)&&delete e[t]}),t&&"object"==typeof t.container&&t.container.jquery&&(t.container=t.container[0]),"number"==typeof(t={...this.constructor.Default,...e,..."object"==typeof t&&t?t:{}}).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),i(Ui,t,this.constructor.DefaultType),t.sanitize&&(t.template=zi(t.template,t.allowList,t.sanitizeFn)),t}_getDelegateConfig(){var t={};if(this.config)for(const e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t}_cleanTipClass(){const e=this.getTipElement();var t=e.getAttribute("class").match(Xi);null!==t&&0<t.length&&t.map(t=>t.trim()).forEach(t=>e.classList.remove(t))}_handlePopperPlacementChange(t){t=t.state;t&&(this.tip=t.elements.popper,this._cleanTipClass(),this._addAttachmentClass(this._getAttachment(t.placement)))}static jQueryInterface(i){return this.each(function(){let t=a(this,Gi);var e="object"==typeof i&&i;if((t||!/dispose|hide/.test(i))&&(t=t||new an(this,e),"string"==typeof i)){if(void 0===t[i])throw new TypeError(`No method named "${i}"`);t[i]()}})}}t(Ui,an);const ln="popover",cn="bs.popover",P="."+cn,dn="bs-popover",un=new RegExp(`(^|\\s)${dn}\\S+`,"g"),hn={...an.Default,placement:"right",offset:[0,8],trigger:"click",content:"",template:'<div class="popover" role="tooltip"><div class="popover-arrow"></div><h3 class="popover-header"></h3><div class="popover-body"></div></div>'},pn={...an.DefaultType,content:"(string|element|function)"},fn={HIDE:"hide"+P,HIDDEN:"hidden"+P,SHOW:"show"+P,SHOWN:"shown"+P,INSERTED:"inserted"+P,CLICK:"click"+P,FOCUSIN:"focusin"+P,FOCUSOUT:"focusout"+P,MOUSEENTER:"mouseenter"+P,MOUSELEAVE:"mouseleave"+P};class mn extends an{static get Default(){return hn}static get NAME(){return ln}static get DATA_KEY(){return cn}static get Event(){return fn}static get EVENT_KEY(){return P}static get DefaultType(){return pn}isWithContent(){return this.getTitle()||this._getContent()}setContent(){var t=this.getTipElement();this.setElementContent(h.findOne(".popover-header",t),this.getTitle());let e=this._getContent();"function"==typeof e&&(e=e.call(this._element)),this.setElementContent(h.findOne(".popover-body",t),e),t.classList.remove("fade","show")}_addAttachmentClass(t){this.getTipElement().classList.add(dn+"-"+this.updateAttachment(t))}_getContent(){return this._element.getAttribute("data-bs-content")||this.config.content}_cleanTipClass(){const e=this.getTipElement();var t=e.getAttribute("class").match(un);null!==t&&0<t.length&&t.map(t=>t.trim()).forEach(t=>e.classList.remove(t))}static jQueryInterface(i){return this.each(function(){let t=a(this,cn);var e="object"==typeof i?i:null;if((t||!/dispose|hide/.test(i))&&(t||(t=new mn(this,e),G(this,cn,t)),"string"==typeof i)){if(void 0===t[i])throw new TypeError(`No method named "${i}"`);t[i]()}})}}t(ln,mn);const gn="scrollspy",yn="bs.scrollspy",vn="."+yn;const bn={offset:10,method:"auto",target:""},wn={offset:"number",method:"string",target:"(string|element)"};vn,vn;vn;const _n="dropdown-item",kn="active",xn=".nav-link",Tn=".list-group-item",Sn="position";class Cn extends s{constructor(t,e){super(t),this._scrollElement="BODY"===this._element.tagName?window:this._element,this._config=this._getConfig(e),this._selector=`${this._config.target} ${xn}, ${this._config.target} ${Tn}, ${this._config.target} .`+_n,this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,m.on(this._scrollElement,"scroll.bs.scrollspy",()=>this._process()),this.refresh(),this._process()}static get Default(){return bn}static get DATA_KEY(){return yn}refresh(){var t=this._scrollElement===this._scrollElement.window?"offset":Sn;const n="auto"===this._config.method?t:this._config.method,o=n===Sn?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),h.find(this._selector).map(t=>{var t=F(t),e=t?h.findOne(t):null;if(e){var i=e.getBoundingClientRect();if(i.width||i.height)return[l[n](e).top+o,t]}return null}).filter(t=>t).sort((t,e)=>t[0]-e[0]).forEach(t=>{this._offsets.push(t[0]),this._targets.push(t[1])})}dispose(){super.dispose(),m.off(this._scrollElement,vn),this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null}_getConfig(e){if("string"!=typeof(e={...bn,..."object"==typeof e&&e?e:{}}).target&&r(e.target)){let t=e.target["id"];t||(t=I(gn),e.target.id=t),e.target="#"+t}return i(gn,e,wn),e}_getScrollTop(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop}_getScrollHeight(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)}_getOffsetHeight(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height}_process(){var e=this._getScrollTop()+this._config.offset,t=this._getScrollHeight(),i=this._config.offset+t-this._getOffsetHeight();if(this._scrollHeight!==t&&this.refresh(),i<=e)t=this._targets[this._targets.length-1],this._activeTarget!==t&&this._activate(t);else if(this._activeTarget&&e<this._offsets[0]&&0<this._offsets[0])this._activeTarget=null,this._clear();else for(let t=this._offsets.length;t--;)this._activeTarget!==this._targets[t]&&e>=this._offsets[t]&&(void 0===this._offsets[t+1]||e<this._offsets[t+1])&&this._activate(this._targets[t])}_activate(e){this._activeTarget=e,this._clear();var t=this._selector.split(",").map(t=>t+`[data-bs-target="${e}"],${t}[href="${e}"]`),t=h.findOne(t.join(","));t.classList.contains(_n)?(h.findOne(".dropdown-toggle",t.closest(".dropdown")).classList.add(kn),t.classList.add(kn)):(t.classList.add(kn),h.parents(t,".nav, .list-group").forEach(t=>{h.prev(t,xn+", "+Tn).forEach(t=>t.classList.add(kn)),h.prev(t,".nav-item").forEach(t=>{h.children(t,xn).forEach(t=>t.classList.add(kn))})})),m.trigger(this._scrollElement,"activate.bs.scrollspy",{relatedTarget:e})}_clear(){h.find(this._selector).filter(t=>t.classList.contains(kn)).forEach(t=>t.classList.remove(kn))}static jQueryInterface(i){return this.each(function(){let t=a(this,yn);var e="object"==typeof i&&i;if(t=t||new Cn(this,e),"string"==typeof i){if(void 0===t[i])throw new TypeError(`No method named "${i}"`);t[i]()}})}}m.on(window,"load.bs.scrollspy.data-api",()=>{h.find('[data-bs-spy="scroll"]').forEach(t=>new Cn(t,l.getDataAttributes(t)))}),t(gn,Cn);const En="bs.tab";En;const Dn="active",An=".active",Ln=":scope > li > .active";class On extends s{static get DATA_KEY(){return En}show(){if(!(this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE&&this._element.classList.contains(Dn)||Y(this._element))){let t;var e=o(this._element),i=this._element.closest(".nav, .list-group"),n=(i&&(n="UL"===i.nodeName||"OL"===i.nodeName?Ln:An,t=(t=h.find(n,i))[t.length-1]),t?m.trigger(t,"hide.bs.tab",{relatedTarget:this._element}):null);m.trigger(this._element,"show.bs.tab",{relatedTarget:t}).defaultPrevented||null!==n&&n.defaultPrevented||(this._activate(this._element,i),n=()=>{m.trigger(t,"hidden.bs.tab",{relatedTarget:this._element}),m.trigger(this._element,"shown.bs.tab",{relatedTarget:t})},e?this._activate(e,e.parentNode,n):n())}}_activate(t,e,i){const n=(!e||"UL"!==e.nodeName&&"OL"!==e.nodeName?h.children(e,An):h.find(Ln,e))[0];var e=i&&n&&n.classList.contains("fade"),o=()=>this._transitionComplete(t,n,i);n&&e?(e=d(n),n.classList.remove("show"),m.one(n,"transitionend",o),u(n,e)):o()}_transitionComplete(t,e,i){var n;e&&(e.classList.remove(Dn),(n=h.findOne(":scope > .dropdown-menu .active",e.parentNode))&&n.classList.remove(Dn),"tab"===e.getAttribute("role"))&&e.setAttribute("aria-selected",!1),t.classList.add(Dn),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),z(t),t.classList.contains("fade")&&t.classList.add("show"),t.parentNode&&t.parentNode.classList.contains("dropdown-menu")&&(t.closest(".dropdown")&&h.find(".dropdown-toggle").forEach(t=>t.classList.add(Dn)),t.setAttribute("aria-expanded",!0)),i&&i()}static jQueryInterface(e){return this.each(function(){var t=a(this,En)||new On(this);if("string"==typeof e){if(void 0===t[e])throw new TypeError(`No method named "${e}"`);t[e]()}})}}m.on(document,"click.bs.tab.data-api",'[data-bs-toggle="tab"], [data-bs-toggle="pill"], [data-bs-toggle="list"]',function(t){t.preventDefault(),(a(this,En)||new On(this)).show()}),t("tab",On);const $n="bs.toast";Qt="."+$n;const Mn="click.dismiss"+Qt,jn="show",Pn="showing",Nn={animation:"boolean",autohide:"boolean",delay:"number"},In={animation:!0,autohide:!0,delay:5e3};class Hn extends s{constructor(t,e){super(t),this._config=this._getConfig(e),this._timeout=null,this._setListeners()}static get DefaultType(){return Nn}static get Default(){return In}static get DATA_KEY(){return $n}show(){var t,e;m.trigger(this._element,"show.bs.toast").defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add("fade"),t=()=>{this._element.classList.remove(Pn),this._element.classList.add(jn),m.trigger(this._element,"shown.bs.toast"),this._config.autohide&&(this._timeout=setTimeout(()=>{this.hide()},this._config.delay))},this._element.classList.remove("hide"),z(this._element),this._element.classList.add(Pn),this._config.animation?(e=d(this._element),m.one(this._element,"transitionend",t),u(this._element,e)):t())}hide(){var t,e;this._element.classList.contains(jn)&&!m.trigger(this._element,"hide.bs.toast").defaultPrevented&&(t=()=>{this._element.classList.add("hide"),m.trigger(this._element,"hidden.bs.toast")},this._element.classList.remove(jn),this._config.animation?(e=d(this._element),m.one(this._element,"transitionend",t),u(this._element,e)):t())}dispose(){this._clearTimeout(),this._element.classList.contains(jn)&&this._element.classList.remove(jn),m.off(this._element,Mn),super.dispose(),this._config=null}_getConfig(t){return t={...In,...l.getDataAttributes(this._element),..."object"==typeof t&&t?t:{}},i("toast",t,this.constructor.DefaultType),t}_setListeners(){m.on(this._element,Mn,'[data-bs-dismiss="toast"]',()=>this.hide())}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(i){return this.each(function(){let t=a(this,$n);var e="object"==typeof i&&i;if(t=t||new Hn(this,e),"string"==typeof i){if(void 0===t[i])throw new TypeError(`No method named "${i}"`);t[i](this)}})}}return t("toast",Hn),{Alert:ct,Button:ht,Carousel:y,Collapse:Ot,Dropdown:$,Modal:Si,Offcanvas:qi,Popover:mn,ScrollSpy:Cn,Tab:On,Toast:Hn,Tooltip:an}}),!function(t){var e;"function"==typeof define&&define.amd?define("jquery-typeahead",["jquery"],t):"object"==typeof module&&module.exports?module.exports=(void 0===e&&(e="undefined"!=typeof window?require("jquery"):require("jquery")(void 0)),t(e)):t(jQuery)}(function(A){"use strict";function r(t,e){this.rawQuery=t.val()||"",this.query=t.val()||"",this.selector=t[0].selector,this.deferred=null,this.tmpSource={},this.source={},this.dynamicGroups=[],this.hasDynamicGroups=!1,this.generatedGroupCount=0,this.groupBy="group",this.groups=[],this.searchGroups=[],this.generateGroups=[],this.requestGroups=[],this.result=[],this.tmpResult={},this.groupTemplate="",this.resultHtml=null,this.resultCount=0,this.resultCountPerGroup={},this.options=e,this.node=t,this.namespace="."+this.helper.slugify.call(this,this.selector)+".typeahead",this.isContentEditable=void 0!==this.node.attr("contenteditable")&&"false"!==this.node.attr("contenteditable"),this.container=null,this.resultContainer=null,this.item=null,this.items=null,this.comparedItems=null,this.xhr={},this.hintIndex=null,this.filters={dropdown:{},dynamic:{}},this.dropdownFilter={static:[],dynamic:[]},this.dropdownFilterAll=null,this.isDropdownEvent=!1,this.requests={},this.backdrop={},this.hint={},this.label={},this.hasDragged=!1,this.focusOnly=!1,this.displayEmptyTemplate,this.__construct()}var i,n={input:null,minLength:2,maxLength:!(window.Typeahead={version:"2.11.1"}),maxItem:8,dynamic:!1,delay:300,order:null,offset:!1,hint:!1,accent:!1,highlight:!0,multiselect:null,group:!1,groupOrder:null,maxItemPerGroup:null,dropdownFilter:!1,dynamicFilter:null,backdrop:!1,backdropOnFocus:!1,cache:!1,ttl:36e5,compression:!1,searchOnFocus:!1,blurOnTab:!0,resultContainer:null,generateOnLoad:null,mustSelectItem:!1,href:null,display:["display"],template:null,templateValue:null,groupTemplate:null,correlativeTemplate:!1,emptyTemplate:!1,cancelButton:!0,loadingAnimation:!0,asyncResult:!1,filter:!0,matcher:null,source:null,callback:{onInit:null,onReady:null,onShowLayout:null,onHideLayout:null,onSearch:null,onResult:null,onLayoutBuiltBefore:null,onLayoutBuiltAfter:null,onNavigateBefore:null,onNavigateAfter:null,onEnter:null,onLeave:null,onClickBefore:null,onClickAfter:null,onDropdownFilter:null,onSendRequest:null,onReceiveRequest:null,onPopulateSource:null,onCacheSave:null,onSubmit:null,onCancel:null},selector:{container:"typeahead__container",result:"typeahead__result",list:"typeahead__list",group:"typeahead__group",item:"typeahead__item",empty:"typeahead__empty",display:"typeahead__display",query:"typeahead__query",filter:"typeahead__filter",filterButton:"typeahead__filter-button",dropdown:"typeahead__dropdown",dropdownItem:"typeahead__dropdown-item",labelContainer:"typeahead__label-container",label:"typeahead__label",button:"typeahead__button",backdrop:"typeahead__backdrop",hint:"typeahead__hint",cancelButton:"typeahead__cancel-button"},debug:!1},o={from:"ãàáäâẽèéëêìíïîõòóöôùúüûñç",to:"aaaaaeeeeeiiiiooooouuuunc"},s=~window.navigator.appVersion.indexOf("MSIE 9."),a=~window.navigator.appVersion.indexOf("MSIE 10"),l=!!~window.navigator.userAgent.indexOf("Trident")&&~window.navigator.userAgent.indexOf("rv:11"),e=(r.prototype={_validateCacheMethod:function(t){var e;if(!0===t)t="localStorage";else if("string"==typeof t&&!~["localStorage","sessionStorage"].indexOf(t))return!1;e=void 0!==window[t];try{window[t].setItem("typeahead","typeahead"),window[t].removeItem("typeahead")}catch(t){e=!1}return e&&t||!1},extendOptions:function(){if(this.options.cache=this._validateCacheMethod(this.options.cache),!this.options.compression||"object"==typeof LZString&&this.options.cache||(this.options.compression=!1),this.options.maxLength&&!isNaN(this.options.maxLength)||(this.options.maxLength=1/0),void 0!==this.options.maxItem&&~[0,!1].indexOf(this.options.maxItem)&&(this.options.maxItem=1/0),this.options.maxItemPerGroup&&!/^\d+$/.test(this.options.maxItemPerGroup)&&(this.options.maxItemPerGroup=null),this.options.display&&!Array.isArray(this.options.display)&&(this.options.display=[this.options.display]),this.options.multiselect&&(this.items=[],this.comparedItems=[],"string"==typeof this.options.multiselect.matchOn)&&(this.options.multiselect.matchOn=[this.options.multiselect.matchOn]),!this.options.group||Array.isArray(this.options.group)||("string"==typeof this.options.group?this.options.group={key:this.options.group}:"boolean"==typeof this.options.group&&(this.options.group={key:"group"}),this.options.group.key=this.options.group.key||"group"),this.options.highlight&&!~["any",!0].indexOf(this.options.highlight)&&(this.options.highlight=!1),this.options.dropdownFilter&&this.options.dropdownFilter instanceof Object){Array.isArray(this.options.dropdownFilter)||(this.options.dropdownFilter=[this.options.dropdownFilter]);for(var t=0,e=this.options.dropdownFilter.length;t<e;++t)this.dropdownFilter[this.options.dropdownFilter[t].value?"static":"dynamic"].push(this.options.dropdownFilter[t])}this.options.dynamicFilter&&!Array.isArray(this.options.dynamicFilter)&&(this.options.dynamicFilter=[this.options.dynamicFilter]),this.options.accent&&("object"==typeof this.options.accent?this.options.accent.from&&this.options.accent.to&&(this.options.accent.from.length,this.options.accent.to.length):this.options.accent=o),this.options.groupTemplate&&(this.groupTemplate=this.options.groupTemplate),this.options.resultContainer&&("string"==typeof this.options.resultContainer&&(this.options.resultContainer=A(this.options.resultContainer)),this.options.resultContainer instanceof A)&&this.options.resultContainer[0]&&(this.resultContainer=this.options.resultContainer),this.options.group&&this.options.group.key&&(this.groupBy=this.options.group.key),this.options.callback&&this.options.callback.onClick&&(this.options.callback.onClickBefore=this.options.callback.onClick,delete this.options.callback.onClick),this.options.callback&&this.options.callback.onNavigate&&(this.options.callback.onNavigateBefore=this.options.callback.onNavigate,delete this.options.callback.onNavigate),this.options=A.extend(!0,{},n,this.options)},unifySourceFormat:function(){var t,e,i;for(t in this.dynamicGroups=[],Array.isArray(this.options.source)&&(this.options.source={group:{data:this.options.source}}),"string"==typeof this.options.source&&(this.options.source={group:{ajax:{url:this.options.source}}}),this.options.source.ajax&&(this.options.source={group:{ajax:this.options.source.ajax}}),(this.options.source.url||this.options.source.data)&&(this.options.source={group:this.options.source}),this.options.source)if(this.options.source.hasOwnProperty(t)){if(i=(e="string"==typeof(e=this.options.source[t])?{ajax:{url:e}}:e).url||e.ajax,Array.isArray(i)?(e.ajax="string"==typeof i[0]?{url:i[0]}:i[0],e.ajax.path=e.ajax.path||i[1]||null):"object"==typeof e.url?e.ajax=e.url:"string"==typeof e.url&&(e.ajax={url:e.url}),delete e.url,!e.data&&!e.ajax)return!1;e.display&&!Array.isArray(e.display)&&(e.display=[e.display]),e.minLength=("number"==typeof e.minLength?e:this.options).minLength,e.maxLength=("number"==typeof e.maxLength?e:this.options).maxLength,e.dynamic="boolean"==typeof e.dynamic||this.options.dynamic,e.minLength>e.maxLength&&(e.minLength=e.maxLength),this.options.source[t]=e,this.options.source[t].dynamic&&this.dynamicGroups.push(t),e.cache=void 0!==e.cache?this._validateCacheMethod(e.cache):this.options.cache,!e.compression||"object"==typeof LZString&&e.cache||(e.compression=!1)}return this.hasDynamicGroups=this.options.dynamic||!!this.dynamicGroups.length,!0},init:function(){this.helper.executeCallback.call(this,this.options.callback.onInit,[this.node]),this.container=this.node.closest("."+this.options.selector.container)},delegateEvents:function(){var e,i=this,t=["focus"+this.namespace,"input"+this.namespace,"propertychange"+this.namespace,"keydown"+this.namespace,"keyup"+this.namespace,"search"+this.namespace,"generate"+this.namespace],n=(A("html").on("touchmove",function(){i.hasDragged=!0}).on("touchstart",function(){i.hasDragged=!1}),this.node.closest("form").on("submit",function(t){if(!i.options.mustSelectItem||!i.helper.isEmpty(i.item))return i.options.backdropOnFocus||i.hideLayout(),i.options.callback.onSubmit?i.helper.executeCallback.call(i,i.options.callback.onSubmit,[i.node,this,i.item||i.items,t]):void 0;t.preventDefault()}).on("reset",function(){setTimeout(function(){i.node.trigger("input"+i.namespace),i.hideLayout()})}),!1);this.node.attr("placeholder")&&(a||l)&&(e=!0,this.node.on("focusin focusout",function(){e=!(this.value||!this.placeholder)}),this.node.on("input",function(t){e&&(t.stopImmediatePropagation(),e=!1)})),this.node.off(this.namespace).on(t.join(" "),function(t,e){switch(t.type){case"generate":i.generateSource(Object.keys(i.options.source));break;case"focus":i.focusOnly?i.focusOnly=!1:(i.options.backdropOnFocus&&(i.buildBackdropLayout(),i.showLayout()),i.options.searchOnFocus&&!i.item&&(i.deferred=A.Deferred(),i.assignQuery(),i.generateSource()));break;case"keydown":8===t.keyCode&&i.options.multiselect&&i.options.multiselect.cancelOnBackspace&&""===i.query&&i.items.length?i.cancelMultiselectItem(i.items.length-1,null,t):t.keyCode&&~[9,13,27,38,39,40].indexOf(t.keyCode)&&(n=!0,i.navigate(t));break;case"keyup":s&&i.node[0].value.replace(/^\s+/,"").toString().length<i.query.length&&i.node.trigger("input"+i.namespace);break;case"propertychange":if(n){n=!1;break}case"input":i.deferred=A.Deferred(),i.assignQuery(),""===i.rawQuery&&""===i.query&&(t.originalEvent=e||{},i.helper.executeCallback.call(i,i.options.callback.onCancel,[i.node,i.item,t]),i.item=null),i.options.cancelButton&&i.toggleCancelButtonVisibility(),i.options.hint&&i.hint.container&&""!==i.hint.container.val()&&0!==i.hint.container.val().indexOf(i.rawQuery)&&(i.hint.container.val(""),i.isContentEditable)&&i.hint.container.text(""),i.hasDynamicGroups?i.helper.typeWatch(function(){i.generateSource()},i.options.delay):i.generateSource();break;case"search":i.searchResult(),i.buildLayout(),i.result.length||i.searchGroups.length&&i.displayEmptyTemplate?i.showLayout():i.hideLayout(),i.deferred&&i.deferred.resolve()}return i.deferred&&i.deferred.promise()}),this.options.generateOnLoad&&this.node.trigger("generate"+this.namespace)},assignQuery:function(){this.isContentEditable?this.rawQuery=this.node.text():this.rawQuery=this.node.val().toString(),this.rawQuery=this.rawQuery.replace(/^\s+/,""),this.rawQuery!==this.query&&(this.query=this.rawQuery)},filterGenerateSource:function(){if(this.searchGroups=[],this.generateGroups=[],!this.focusOnly||this.options.multiselect)for(var t in this.options.source)if(this.options.source.hasOwnProperty(t)&&this.query.length>=this.options.source[t].minLength&&this.query.length<=this.options.source[t].maxLength){if(this.filters.dropdown&&"group"===this.filters.dropdown.key&&this.filters.dropdown.value!==t)continue;if(this.searchGroups.push(t),!this.options.source[t].dynamic&&this.source[t])continue;this.generateGroups.push(t)}},generateSource:function(t){if(this.filterGenerateSource(),this.generatedGroupCount=0,Array.isArray(t)&&t.length)this.generateGroups=t;else if(!this.generateGroups.length)return void this.node.trigger("search"+this.namespace);if(this.requestGroups=[],this.options.loadingAnimation&&this.container.addClass("loading"),!this.helper.isEmpty(this.xhr)){for(var e in this.xhr)this.xhr.hasOwnProperty(e)&&this.xhr[e].abort();this.xhr={}}for(var i,n,o,s,r,a,l=this,c=(e=0,this.generateGroups.length);e<c;++e){if(i=this.generateGroups[e],s=(o=this.options.source[i]).cache,a=o.compression,this.options.asyncResult&&delete this.source[i],s&&(r=window[s].getItem("TYPEAHEAD_"+this.selector+":"+i))){a&&(r=LZString.decompressFromUTF16(r)),a=!1;try{(r=JSON.parse(r+"")).data&&r.ttl>(new Date).getTime()?(this.populateSource(r.data,i),a=!0):window[s].removeItem("TYPEAHEAD_"+this.selector+":"+i)}catch(t){}if(a)continue}!o.data||o.ajax?o.ajax&&(this.requests[i]||(this.requests[i]=this.generateRequestObject(i)),this.requestGroups.push(i)):"function"==typeof o.data?(n=o.data.call(this),Array.isArray(n)?l.populateSource(n,i):"function"==typeof n.promise&&function(e){A.when(n).then(function(t){t&&Array.isArray(t)&&l.populateSource(t,e)})}(i)):this.populateSource(A.extend(!0,[],o.data),i)}return this.requestGroups.length&&this.handleRequests(),this.options.asyncResult&&this.searchGroups.length!==this.generateGroups&&this.node.trigger("search"+this.namespace),!!this.generateGroups.length},generateRequestObject:function(i){var n=this,o=this.options.source[i],t={request:{url:o.ajax.url||null,dataType:"json",beforeSend:function(t,e){n.xhr[i]=t;t=n.requests[i].callback.beforeSend||o.ajax.beforeSend;"function"==typeof t&&t.apply(null,arguments)}},callback:{beforeSend:null,done:null,fail:null,then:null,always:null},extra:{path:o.ajax.path||null,group:i},validForGroup:[i]};if("function"!=typeof o.ajax&&(o.ajax instanceof Object&&(t=this.extendXhrObject(t,o.ajax)),1<Object.keys(this.options.source).length))for(var e in this.requests)!this.requests.hasOwnProperty(e)||this.requests[e].isDuplicated||t.request.url&&t.request.url===this.requests[e].request.url&&(this.requests[e].validForGroup.push(i),t.isDuplicated=!0,delete t.validForGroup);return t},extendXhrObject:function(t,e){return"object"==typeof e.callback&&(t.callback=e.callback,delete e.callback),"function"==typeof e.beforeSend&&(t.callback.beforeSend=e.beforeSend,delete e.beforeSend),t.request=A.extend(!0,t.request,e),"jsonp"!==t.request.dataType.toLowerCase()||t.request.jsonpCallback||(t.request.jsonpCallback="callback_"+t.extra.group),t},handleRequests:function(){var t,c=this,d=this.requestGroups.length;if(!1!==this.helper.executeCallback.call(this,this.options.callback.onSendRequest,[this.node,this.query]))for(var e=0,i=this.requestGroups.length;e<i;++e)t=this.requestGroups[e],this.requests[t].isDuplicated||function(t,r){if("function"==typeof c.options.source[t].ajax){var e=c.options.source[t].ajax.call(c,c.query);if("object"!=typeof(r=c.extendXhrObject(c.generateRequestObject(t),"object"==typeof e?e:{})).request||!r.request.url)return c.populateSource([],t);c.requests[t]=r}var a,i=!1,l={};if(~r.request.url.indexOf("{{query}}")&&(i||(r=A.extend(!0,{},r),i=!0),r.request.url=r.request.url.replace("{{query}}",encodeURIComponent(c.query))),r.request.data)for(var n in r.request.data)if(r.request.data.hasOwnProperty(n)&&~String(r.request.data[n]).indexOf("{{query}}")){i||(r=A.extend(!0,{},r),i=!0),r.request.data[n]=r.request.data[n].replace("{{query}}",c.query);break}A.ajax(r.request).done(function(t,e,i){for(var n,o=0,s=r.validForGroup.length;o<s;o++)n=r.validForGroup[o],"function"==typeof(a=c.requests[n]).callback.done&&(l[n]=a.callback.done.call(c,t,e,i))}).fail(function(t,e,i){for(var n=0,o=r.validForGroup.length;n<o;n++)(a=c.requests[r.validForGroup[n]]).callback.fail instanceof Function&&a.callback.fail.call(c,t,e,i)}).always(function(t,e,i){for(var n,o=0,s=r.validForGroup.length;o<s;o++){if(n=r.validForGroup[o],(a=c.requests[n]).callback.always instanceof Function&&a.callback.always.call(c,t,e,i),"abort"===e)return;c.populateSource((null!==t&&"function"==typeof t.promise?[]:l[n])||t,a.extra.group,a.extra.path||a.request.path),0==--d&&c.helper.executeCallback.call(c,c.options.callback.onReceiveRequest,[c.node,c.query])}}).then(function(t,e){for(var i=0,n=r.validForGroup.length;i<n;i++)(a=c.requests[r.validForGroup[i]]).callback.then instanceof Function&&a.callback.then.call(c,t,e)})}(t,this.requests[t])},populateSource:function(i,t,e){var n=this,o=this.options.source[t],s=o.ajax&&o.data;e&&"string"==typeof e&&(i=this.helper.namespace.call(this,e,i)),Array.isArray(i)||(i=[]),s&&("function"==typeof s&&(s=s()),Array.isArray(s))&&(i=i.concat(s));for(var r,a=o.display?"compiled"===o.display[0]?o.display[1]:o.display[0]:"compiled"===this.options.display[0]?this.options.display[1]:this.options.display[0],l=0,c=i.length;l<c;l++)null!==i[l]&&"boolean"!=typeof i[l]&&("string"==typeof i[l]&&((r={})[a]=i[l],i[l]=r),i[l].group=t);if(!this.hasDynamicGroups&&this.dropdownFilter.dynamic.length)for(var d,u,h={},l=0,c=i.length;l<c;l++)for(var p=0,f=this.dropdownFilter.dynamic.length;p<f;p++)d=this.dropdownFilter.dynamic[p].key,(u=i[l][d])&&(this.dropdownFilter.dynamic[p].value||(this.dropdownFilter.dynamic[p].value=[]),h[d]||(h[d]=[]),~h[d].indexOf(u.toLowerCase())||(h[d].push(u.toLowerCase()),this.dropdownFilter.dynamic[p].value.push(u)));if(this.options.correlativeTemplate){var s=o.template||this.options.template,m="";if(s="function"==typeof s?s.call(this,"",{}):s){if(Array.isArray(this.options.correlativeTemplate))for(l=0,c=this.options.correlativeTemplate.length;l<c;l++)m+="{{"+this.options.correlativeTemplate[l]+"}} ";else m=s.replace(/<.+?>/g," ").replace(/\s{2,}/," ").trim();for(l=0,c=i.length;l<c;l++)i[l].compiled=A("<textarea />").html(m.replace(/\{\{([\w\-\.]+)(?:\|(\w+))?}}/g,function(t,e){return n.helper.namespace.call(n,e,i[l],"get","")}).trim()).text();o.display?~o.display.indexOf("compiled")||o.display.unshift("compiled"):~this.options.display.indexOf("compiled")||this.options.display.unshift("compiled")}}this.options.callback.onPopulateSource&&(i=this.helper.executeCallback.call(this,this.options.callback.onPopulateSource,[this.node,i,t,e])),this.tmpSource[t]=Array.isArray(i)&&i||[];var s=this.options.source[t].cache,o=this.options.source[t].compression,g=this.options.source[t].ttl||this.options.ttl;s&&!window[s].getItem("TYPEAHEAD_"+this.selector+":"+t)&&(this.options.callback.onCacheSave&&(i=this.helper.executeCallback.call(this,this.options.callback.onCacheSave,[this.node,i,t,e])),e=JSON.stringify({data:i,ttl:(new Date).getTime()+g}),o&&(e=LZString.compressToUTF16(e)),window[s].setItem("TYPEAHEAD_"+this.selector+":"+t,e)),this.incrementGeneratedGroup(t)},incrementGeneratedGroup:function(t){if(this.generatedGroupCount++,this.generatedGroupCount===this.generateGroups.length||this.options.asyncResult){this.xhr&&this.xhr[t]&&delete this.xhr[t];for(var e=0,i=this.generateGroups.length;e<i;e++)this.source[this.generateGroups[e]]=this.tmpSource[this.generateGroups[e]];this.hasDynamicGroups||this.buildDropdownItemLayout("dynamic"),this.generatedGroupCount===this.generateGroups.length&&(this.xhr={},this.options.loadingAnimation)&&this.container.removeClass("loading"),this.node.trigger("search"+this.namespace)}},navigate:function(t){var e,i,n,o,s;this.helper.executeCallback.call(this,this.options.callback.onNavigateBefore,[this.node,this.query,t]),27===t.keyCode?(t.preventDefault(),this.query.length?(this.resetInput(),this.node.trigger("input"+this.namespace,[t])):(this.node.blur(),this.hideLayout())):this.result.length&&(i=(s=(e=this.resultContainer.find("."+this.options.selector.item).not("[disabled]")).filter(".active"))[0]?e.index(s):null,n=s[0]?s.attr("data-index"):null,o=null,this.clearActiveItem(),this.helper.executeCallback.call(this,this.options.callback.onLeave,[this.node,null!==i&&e.eq(i)||void 0,null!==n&&this.result[n]||void 0,t]),13===t.keyCode?(t.preventDefault(),0<s.length?"javascript:;"===s.find("a:first")[0].href?s.find("a:first").trigger("click",t):s.find("a:first")[0].click():this.node.closest("form").trigger("submit")):39!==t.keyCode?(9===t.keyCode?this.options.blurOnTab?this.hideLayout():0<s.length?i+1<e.length?(t.preventDefault(),this.addActiveItem(e.eq(o=i+1))):this.hideLayout():e.length?(t.preventDefault(),o=0,this.addActiveItem(e.first())):this.hideLayout():38===t.keyCode?(t.preventDefault(),0<s.length?0<=i-1&&this.addActiveItem(e.eq(o=i-1)):e.length&&(o=e.length-1,this.addActiveItem(e.last()))):40===t.keyCode&&(t.preventDefault(),0<s.length?i+1<e.length&&this.addActiveItem(e.eq(o=i+1)):e.length&&(o=0,this.addActiveItem(e.first()))),n=null!==o?e.eq(o).attr("data-index"):null,this.helper.executeCallback.call(this,this.options.callback.onEnter,[this.node,null!==o&&e.eq(o)||void 0,null!==n&&this.result[n]||void 0,t]),t.preventInputChange&&~[38,40].indexOf(t.keyCode)&&this.buildHintLayout(null!==n&&n<this.result.length?[this.result[n]]:null),this.options.hint&&this.hint.container&&this.hint.container.css("color",t.preventInputChange?this.hint.css.color:null===n&&this.hint.css.color||this.hint.container.css("background-color")||"fff"),s=null===n||t.preventInputChange?this.rawQuery:this.getTemplateValue.call(this,this.result[n]),this.node.val(s),this.isContentEditable&&this.node.text(s),this.helper.executeCallback.call(this,this.options.callback.onNavigateAfter,[this.node,e,null!==o&&e.eq(o).find("a:first")||void 0,null!==n&&this.result[n]||void 0,this.query,t])):null!==i?e.eq(i).find("a:first")[0].click():this.options.hint&&""!==this.hint.container.val()&&this.helper.getCaret(this.node[0])>=this.query.length&&e.filter('[data-index="'+this.hintIndex+'"]').find("a:first")[0].click())},getTemplateValue:function(i){var t,n;if(i)return(t="function"==typeof(t=i.group&&this.options.source[i.group].templateValue||this.options.templateValue)?t.call(this):t)?(n=this,t.replace(/\{\{([\w\-.]+)}}/gi,function(t,e){return n.helper.namespace.call(n,e,i,"get","")})):this.helper.namespace.call(this,i.matchedKey,i).toString()},clearActiveItem:function(){this.resultContainer.find("."+this.options.selector.item).removeClass("active")},addActiveItem:function(t){t.addClass("active")},searchResult:function(){this.resetLayout(),!1!==this.helper.executeCallback.call(this,this.options.callback.onSearch,[this.node,this.query])&&(!this.searchGroups.length||this.options.multiselect&&this.options.multiselect.limit&&this.items.length>=this.options.multiselect.limit||this.searchResultData(),this.helper.executeCallback.call(this,this.options.callback.onResult,[this.node,this.query,this.result,this.resultCount,this.resultCountPerGroup]),this.isDropdownEvent)&&(this.helper.executeCallback.call(this,this.options.callback.onDropdownFilter,[this.node,this.query,this.filters.dropdown,this.result]),this.isDropdownEvent=!1)},searchResultData:function(){var t,e,i,n,o,s,r,a,l=this.groupBy,c=this.query.toLowerCase(),d=this.options.maxItem,u=this.options.maxItemPerGroup,h=this.filters.dynamic&&!this.helper.isEmpty(this.filters.dynamic),p="function"==typeof this.options.matcher&&this.options.matcher;this.options.accent&&(c=this.helper.removeAccent.call(this,c));for(var f=0,m=this.searchGroups.length;f<m;++f)if(S=this.searchGroups[f],(!this.filters.dropdown||"group"!==this.filters.dropdown.key||this.filters.dropdown.value===S)&&(i=(void 0!==this.options.source[S].filter?this.options.source[S]:this.options).filter,o="function"==typeof this.options.source[S].matcher&&this.options.source[S].matcher||p,this.source[S]))for(var g=0,y=this.source[S].length;g<y&&(!(this.resultItemCount>=d)||this.options.callback.onResult);g++)if((!h||this.dynamicFilter.validate.apply(this,[this.source[S][g]]))&&null!==(t=this.source[S][g])&&"boolean"!=typeof t&&(!this.options.multiselect||this.isMultiselectUniqueData(t))&&(!this.filters.dropdown||(t[this.filters.dropdown.key]||"").toLowerCase()===(this.filters.dropdown.value||"").toLowerCase())){if((a="group"===l?S:t[l]||t.group)&&!this.tmpResult[a]&&(this.tmpResult[a]=[],this.resultCountPerGroup[a]=0),u&&"group"===l&&this.tmpResult[a].length>=u&&!this.options.callback.onResult)break;for(var v=0,b=(C=this.options.source[S].display||this.options.display).length;v<b;++v){if(!1!==i){if(void 0===(e=/\./.test(C[v])?this.helper.namespace.call(this,C[v],t):t[C[v]])||""===e)continue;e=this.helper.cleanStringFromScript(e)}if("function"==typeof i){if(void 0===(n=i.call(this,t,e)))break;if(!n)continue;"object"==typeof n&&(t=n)}if(~[void 0,!0].indexOf(i)){if(null===e)continue;if(n=(n=e).toString().toLowerCase(),s=(n=this.options.accent?this.helper.removeAccent.call(this,n):n).indexOf(c),this.options.correlativeTemplate&&"compiled"===C[v]&&s<0&&/\s/.test(c))for(var w=!0,_=n,k=0,x=(r=c.split(" ")).length;k<x;k++)if(""!==r[k]){if(!~_.indexOf(r[k])){w=!1;break}_=_.replace(r[k],"")}if(s<0&&!w)continue;if(this.options.offset&&0!==s)continue;if(o){if(void 0===(s=o.call(this,t,e)))break;if(!s)continue;"object"==typeof s&&(t=s)}}if(this.resultCount++,this.resultCountPerGroup[a]++,this.resultItemCount<d){if(u&&this.tmpResult[a].length>=u)break;this.tmpResult[a].push(A.extend(!0,{matchedKey:C[v]},t)),this.resultItemCount++}break}if(!this.options.callback.onResult){if(this.resultItemCount>=d)break;if(u&&this.tmpResult[a].length>=u&&"group"===l)break}}if(this.options.order){var T,S,C=[];for(S in this.tmpResult)if(this.tmpResult.hasOwnProperty(S)){for(f=0,m=this.tmpResult[S].length;f<m;f++)T=this.options.source[this.tmpResult[S][f].group].display||this.options.display,~C.indexOf(T[0])||C.push(T[0]);this.tmpResult[S].sort(this.helper.sort(C,"asc"===this.options.order,function(t){return t?t.toString().toUpperCase():""}))}}for(var E,D=[],f=0,m=(E="function"==typeof this.options.groupOrder?this.options.groupOrder.apply(this,[this.node,this.query,this.tmpResult,this.resultCount,this.resultCountPerGroup]):Array.isArray(this.options.groupOrder)?this.options.groupOrder:"string"==typeof this.options.groupOrder&&~["asc","desc"].indexOf(this.options.groupOrder)?Object.keys(this.tmpResult).sort(this.helper.sort([],"asc"===this.options.groupOrder,function(t){return t.toString().toUpperCase()})):Object.keys(this.tmpResult)).length;f<m;f++)D=D.concat(this.tmpResult[E[f]]||[]);this.groups=JSON.parse(JSON.stringify(E)),this.result=D},buildLayout:function(){this.buildHtmlLayout(),this.buildBackdropLayout(),this.buildHintLayout(),this.options.callback.onLayoutBuiltBefore&&(this.tmpResultHtml=this.helper.executeCallback.call(this,this.options.callback.onLayoutBuiltBefore,[this.node,this.query,this.result,this.resultHtml])),this.tmpResultHtml instanceof A?this.resultContainer.html(this.tmpResultHtml):this.resultHtml instanceof A&&this.resultContainer.html(this.resultHtml),this.options.callback.onLayoutBuiltAfter&&this.helper.executeCallback.call(this,this.options.callback.onLayoutBuiltAfter,[this.node,this.query,this.result])},buildHtmlLayout:function(){if(!1!==this.options.resultContainer){var c;if(this.resultContainer||(this.resultContainer=A("<div/>",{class:this.options.selector.result}),this.container.append(this.resultContainer)),!this.result.length&&this.generatedGroupCount===this.generateGroups.length)if(this.options.multiselect&&this.options.multiselect.limit&&this.items.length>=this.options.multiselect.limit)c=this.options.multiselect.limitTemplate?"function"==typeof this.options.multiselect.limitTemplate?this.options.multiselect.limitTemplate.call(this,this.query):this.options.multiselect.limitTemplate.replace(/\{\{query}}/gi,A("<div>").text(this.helper.cleanStringFromScript(this.query)).html()):"Can't select more than "+this.items.length+" items.";else{if(!this.options.emptyTemplate||""===this.query)return;c="function"==typeof this.options.emptyTemplate?this.options.emptyTemplate.call(this,this.query):this.options.emptyTemplate.replace(/\{\{query}}/gi,A("<div>").text(this.helper.cleanStringFromScript(this.query)).html())}this.displayEmptyTemplate=!!c;var o=this.query.toLowerCase(),d=(this.options.accent&&(o=this.helper.removeAccent.call(this,o)),this),t=this.groupTemplate||"<ul></ul>",u=!1;this.groupTemplate?t=A(t.replace(/<([^>]+)>\{\{(.+?)}}<\/[^>]+>/g,function(t,e,i,n,o){var s="",r="group"===i?d.groups:[i];if(!d.result.length)return!0===u?"":(u=!0,"<"+e+' class="'+d.options.selector.empty+'">'+c+"</"+e+">");for(var a=0,l=r.length;a<l;++a)s+="<"+e+' data-group-template="'+r[a]+'"><ul></ul></'+e+">";return s})):(t=A(t),this.result.length||t.append(c instanceof A?c:'<li class="'+d.options.selector.empty+'">'+c+"</li>")),t.addClass(this.options.selector.list+(this.helper.isEmpty(this.result)?" empty":""));for(var e,i,s,n,r,a,l,h,p,f,m=this.groupTemplate&&this.result.length&&d.groups||[],g=0,y=this.result.length;g<y;++g)e=(s=this.result[g]).group,n=!this.options.multiselect&&this.options.source[s.group].href||this.options.href,l=[],h=this.options.source[s.group].display||this.options.display,this.options.group&&(e=s[this.options.group.key],this.options.group.template&&("function"==typeof this.options.group.template?i=this.options.group.template.call(this,s):"string"==typeof this.options.group.template&&(i=this.options.group.template.replace(/\{\{([\w\-\.]+)}}/gi,function(t,e){return d.helper.namespace.call(d,e,s,"get","")}))),t.find('[data-search-group="'+e+'"]')[0]||(this.groupTemplate?t.find('[data-group-template="'+e+'"] ul'):t).append(A("<li/>",{class:d.options.selector.group,html:A("<a/>",{href:"javascript:;",html:i||e,tabindex:-1}),"data-search-group":e}))),this.groupTemplate&&m.length&&~(f=m.indexOf(e||s.group))&&m.splice(f,1),f=A("<li/>",{class:d.options.selector.item+" "+d.options.selector.group+"-"+this.helper.slugify.call(this,e),disabled:!!s.disabled,"data-group":e,"data-index":g,html:A("<a/>",{href:n&&!s.disabled?s.href=d.generateHref.call(d,n,s):"javascript:;",html:function(){if(r=s.group&&d.options.source[s.group].template||d.options.template)"function"==typeof r&&(r=r.call(d,d.query,s)),a=r.replace(/\{\{([^\|}]+)(?:\|([^}]+))*}}/gi,function(t,e,i){var n=d.helper.cleanStringFromScript(String(d.helper.namespace.call(d,e,s,"get","")));return~(i=i&&i.split("|")||[]).indexOf("slugify")&&(n=d.helper.slugify.call(d,n)),~i.indexOf("raw")||!0===d.options.highlight&&o&&~h.indexOf(e)&&(n=d.helper.highlight.call(d,n,o.split(" "),d.options.accent)),n});else{for(var t=0,e=h.length;t<e;t++)void 0!==(p=/\./.test(h[t])?d.helper.namespace.call(d,h[t],s,"get",""):s[h[t]])&&""!==p&&l.push(p);a='<span class="'+d.options.selector.display+'">'+d.helper.cleanStringFromScript(String(l.join(" ")))+"</span>"}(!0===d.options.highlight&&o&&!r||"any"===d.options.highlight)&&(a=d.helper.highlight.call(d,a,o.split(" "),d.options.accent)),A(this).append(a)}})}),function(i,t){t.on("click",function(t,e){i.disabled||(e&&"object"==typeof e&&(t.originalEvent=e),d.options.mustSelectItem&&d.helper.isEmpty(i))?t.preventDefault():(d.options.multiselect||(d.item=i),!1===d.helper.executeCallback.call(d,d.options.callback.onClickBefore,[d.node,A(this),i,t])||t.originalEvent&&t.originalEvent.defaultPrevented||t.isDefaultPrevented()||(d.options.multiselect?(d.query=d.rawQuery="",d.addMultiselectItemLayout(i)):(d.focusOnly=!0,d.query=d.rawQuery=d.getTemplateValue.call(d,i),d.isContentEditable&&(d.node.text(d.query),d.helper.setCaretAtEnd(d.node[0]))),d.hideLayout(),d.node.val(d.query).focus(),d.options.cancelButton&&d.toggleCancelButtonVisibility(),d.helper.executeCallback.call(d,d.options.callback.onClickAfter,[d.node,A(this),i,t])))}),t.on("mouseenter",function(t){i.disabled||(d.clearActiveItem(),d.addActiveItem(A(this))),d.helper.executeCallback.call(d,d.options.callback.onEnter,[d.node,A(this),i,t])}),t.on("mouseleave",function(t){i.disabled||d.clearActiveItem(),d.helper.executeCallback.call(d,d.options.callback.onLeave,[d.node,A(this),i,t])})}(s,f),(this.groupTemplate?t.find('[data-group-template="'+e+'"] ul'):t).append(f);if(this.result.length&&m.length)for(g=0,y=m.length;g<y;++g)t.find('[data-group-template="'+m[g]+'"]').remove();this.resultHtml=t}},generateHref:function(t,n){var o=this;return"string"==typeof t?t=t.replace(/\{\{([^\|}]+)(?:\|([^}]+))*}}/gi,function(t,e,i){e=o.helper.namespace.call(o,e,n,"get","");return e=~(i=i&&i.split("|")||[]).indexOf("slugify")?o.helper.slugify.call(o,e):e}):"function"==typeof t&&(t=t.call(this,n)),t},getMultiselectComparedData:function(t){var e="";if(Array.isArray(this.options.multiselect.matchOn))for(var i=0,n=this.options.multiselect.matchOn.length;i<n;++i)e+=void 0!==t[this.options.multiselect.matchOn[i]]?t[this.options.multiselect.matchOn[i]]:"";else{for(var o=JSON.parse(JSON.stringify(t)),s=["group","matchedKey","compiled","href"],i=0,n=s.length;i<n;++i)delete o[s[i]];e=JSON.stringify(o)}return e},buildBackdropLayout:function(){this.options.backdrop&&(this.backdrop.container||(this.backdrop.css=A.extend({opacity:.6,filter:"alpha(opacity=60)",position:"fixed",top:0,right:0,bottom:0,left:0,"z-index":1040,"background-color":"#000"},this.options.backdrop),this.backdrop.container=A("<div/>",{class:this.options.selector.backdrop,css:this.backdrop.css}).insertAfter(this.container)),this.container.addClass("backdrop").css({"z-index":this.backdrop.css["z-index"]+1,position:"relative"}))},buildHintLayout:function(t){if(this.options.hint)if(this.node[0].scrollWidth>Math.ceil(this.node.innerWidth()))this.hint.container&&this.hint.container.val("");else{var e=this,i="",n=(t=t||this.result,this.query.toLowerCase());if(this.options.accent&&(n=this.helper.removeAccent.call(this,n)),this.hintIndex=null,this.searchGroups.length){if(this.hint.container||(this.hint.css=A.extend({"border-color":"transparent",position:"absolute",top:0,display:"inline","z-index":-1,float:"none",color:"silver","box-shadow":"none",cursor:"default","-webkit-user-select":"none","-moz-user-select":"none","-ms-user-select":"none","user-select":"none"},this.options.hint),this.hint.container=A("<"+this.node[0].nodeName+"/>",{type:this.node.attr("type"),class:this.node.attr("class"),readonly:!0,unselectable:"on","aria-hidden":"true",tabindex:-1,click:function(){e.node.focus()}}).addClass(this.options.selector.hint).css(this.hint.css).insertAfter(this.node),this.node.parent().css({position:"relative"})),this.hint.container.css("color",this.hint.css.color),n)for(var o,s,r=0,a=t.length;r<a;r++)if(!t[r].disabled){for(var l=t[r].group,c=0,d=(o=this.options.source[l].display||this.options.display).length;c<d;c++)if(s=String(t[r][o[c]]).toLowerCase(),0===(s=this.options.accent?this.helper.removeAccent.call(this,s):s).indexOf(n)){i=String(t[r][o[c]]),this.hintIndex=r;break}if(null!==this.hintIndex)break}var u=0<i.length&&this.rawQuery+i.substring(this.query.length)||"";this.hint.container.val(u),this.isContentEditable&&this.hint.container.text(u)}}},buildDropdownLayout:function(){var i;this.options.dropdownFilter&&A("<span/>",{class:(i=this).options.selector.filter,html:function(){A(this).append(A("<button/>",{type:"button",class:i.options.selector.filterButton,style:"display: none;",click:function(){i.container.toggleClass("filter");var e=i.namespace+"-dropdown-filter";A("html").off(e),i.container.hasClass("filter")&&A("html").on("click"+e+" touchend"+e,function(t){A(t.target).closest("."+i.options.selector.filter)[0]&&A(t.target).closest(i.container)[0]||i.hasDragged||(i.container.removeClass("filter"),A("html").off(e))})}})),A(this).append(A("<ul/>",{class:i.options.selector.dropdown}))}}).insertAfter(i.container.find("."+i.options.selector.query))},buildDropdownItemLayout:function(t){if(this.options.dropdownFilter){var e,i,o=this,s="string"==typeof this.options.dropdownFilter&&this.options.dropdownFilter||"All",r=this.container.find("."+this.options.selector.dropdown);"static"!==t||!0!==this.options.dropdownFilter&&"string"!=typeof this.options.dropdownFilter||this.dropdownFilter.static.push({key:"group",template:"{{group}}",all:s,value:Object.keys(this.options.source)});for(var n=0,a=this.dropdownFilter[t].length;n<a;n++){i=this.dropdownFilter[t][n],Array.isArray(i.value)||(i.value=[i.value]),i.all&&(this.dropdownFilterAll=i.all);for(var l=0,c=i.value.length;l<=c;l++)l===c&&n!==a-1||l===c&&n===a-1&&"static"===t&&this.dropdownFilter.dynamic.length||(e=this.dropdownFilterAll||s,i.value[l]?e=i.template?i.template.replace(new RegExp("{{"+i.key+"}}","gi"),i.value[l]):i.value[l]:this.container.find("."+o.options.selector.filterButton).html(e),function(e,i,n){r.append(A("<li/>",{class:o.options.selector.dropdownItem+" "+o.helper.slugify.call(o,i.key+"-"+(i.value[e]||s)),html:A("<a/>",{href:"javascript:;",html:n,click:function(t){t.preventDefault(),d.call(o,{key:i.key,value:i.value[e]||"*",template:n})}})}))}(l,i,e))}this.dropdownFilter[t].length&&this.container.find("."+o.options.selector.filterButton).removeAttr("style")}function d(t){"*"===t.value?delete this.filters.dropdown:this.filters.dropdown=t,this.container.removeClass("filter").find("."+this.options.selector.filterButton).html(t.template),this.isDropdownEvent=!0,this.node.trigger("input"+this.namespace),this.options.multiselect&&this.adjustInputSize(),this.node.focus()}},dynamicFilter:{init:function(){this.options.dynamicFilter&&(this.dynamicFilter.bind.call(this),this.isDynamicFilterEnabled=!0)},validate:function(t){var e,i,n,o=null,s=null;for(n in this.filters.dynamic)if(this.filters.dynamic.hasOwnProperty(n)&&(i=~n.indexOf(".")?this.helper.namespace.call(this,n,t,"get"):t[n],"|"===this.filters.dynamic[n].modifier&&(o=o||(i==this.filters.dynamic[n].value||!1)),"&"===this.filters.dynamic[n].modifier)){if(i!=this.filters.dynamic[n].value){s=!1;break}s=!0}return e=o,!!(e=null!==s&&!0===(e=s)&&null!==o?o:e)},set:function(t,e){t=t.match(/^([|&])?(.+)/);e?this.filters.dynamic[t[2]]={modifier:t[1]||"|",value:e}:delete this.filters.dynamic[t[2]],this.isDynamicFilterEnabled&&this.generateSource()},bind:function(){for(var t,e=this,i=0,n=this.options.dynamicFilter.length;i<n;i++)"string"==typeof(t=this.options.dynamicFilter[i]).selector&&(t.selector=A(t.selector)),t.selector instanceof A&&t.selector[0]&&t.key&&function(t){t.selector.off(e.namespace).on("change"+e.namespace,function(){e.dynamicFilter.set.apply(e,[t.key,e.dynamicFilter.getValue(this)])}).trigger("change"+e.namespace)}(t)},getValue:function(t){var e;return"SELECT"===t.tagName?e=t.value:"INPUT"===t.tagName&&("checkbox"===t.type?e=t.checked&&t.getAttribute("value")||t.checked||null:"radio"===t.type&&t.checked&&(e=t.value)),e}},buildMultiselectLayout:function(){var t,e;this.options.multiselect&&((e=this).label.container=A("<span/>",{class:this.options.selector.labelContainer,"data-padding-left":parseFloat(this.node.css("padding-left"))||0,"data-padding-right":parseFloat(this.node.css("padding-right"))||0,"data-padding-top":parseFloat(this.node.css("padding-top"))||0,click:function(t){A(t.target).hasClass(e.options.selector.labelContainer)&&e.node.focus()}}),this.node.closest("."+this.options.selector.query).prepend(this.label.container),this.options.multiselect.data)&&(Array.isArray(this.options.multiselect.data)?this.populateMultiselectData(this.options.multiselect.data):"function"==typeof this.options.multiselect.data&&(t=this.options.multiselect.data.call(this),Array.isArray(t)?this.populateMultiselectData(t):"function"==typeof t.promise&&A.when(t).then(function(t){t&&Array.isArray(t)&&e.populateMultiselectData(t)})))},isMultiselectUniqueData:function(t){for(var e=!0,i=0,n=this.comparedItems.length;i<n;++i)if(this.comparedItems[i]===this.getMultiselectComparedData(t)){e=!1;break}return e},populateMultiselectData:function(t){for(var e=0,i=t.length;e<i;++e)this.addMultiselectItemLayout(t[e]);this.node.trigger("search"+this.namespace,{origin:"populateMultiselectData"})},addMultiselectItemLayout:function(t){var n,e;if(this.isMultiselectUniqueData(t))return this.items.push(t),this.comparedItems.push(this.getMultiselectComparedData(t)),t=this.getTemplateValue(t),e=(n=this).options.multiselect.href?"a":"span",(t=A("<span/>",{class:this.options.selector.label,html:A("<"+e+"/>",{text:t,click:function(t){var e=A(this).closest("."+n.options.selector.label),e=n.label.container.find("."+n.options.selector.label).index(e);n.options.multiselect.callback&&n.helper.executeCallback.call(n,n.options.multiselect.callback.onClick,[n.node,n.items[e],t])},href:this.options.multiselect.href?(e=n.items[n.items.length-1],n.generateHref.call(n,n.options.multiselect.href,e)):null})})).append(A("<span/>",{class:this.options.selector.cancelButton,html:"×",click:function(t){var e=A(this).closest("."+n.options.selector.label),i=n.label.container.find("."+n.options.selector.label).index(e);n.cancelMultiselectItem(i,e,t)}})),this.label.container.append(t),this.adjustInputSize(),!0},cancelMultiselectItem:function(t,e,i){var n=this.items[t];(e=e||this.label.container.find("."+this.options.selector.label).eq(t)).remove(),this.items.splice(t,1),this.comparedItems.splice(t,1),this.options.multiselect.callback&&this.helper.executeCallback.call(this,this.options.multiselect.callback.onCancel,[this.node,n,i]),this.adjustInputSize(),this.focusOnly=!0,this.node.focus().trigger("input"+this.namespace,{origin:"cancelMultiselectItem"})},adjustInputSize:function(){var i,n=this.node[0].getBoundingClientRect().width-(parseFloat(this.label.container.data("padding-right"))||0)-(parseFloat(this.label.container.css("padding-left"))||0),o=0,s=0,r=!1,a=0,t=(this.label.container.find("."+this.options.selector.label).filter(function(t,e){0===t&&(a=A(e)[0].getBoundingClientRect().height+parseFloat(A(e).css("margin-bottom")||0)),i=A(e)[0].getBoundingClientRect().width+parseFloat(A(e).css("margin-right")||0),.7*n<s+i&&!r&&(o++,r=!0),s+i<n?s+=i:(r=!1,s=i)}),parseFloat(this.label.container.data("padding-left")||0)+(r?0:s)),e=o*a+parseFloat(this.label.container.data("padding-top")||0);this.container.find("."+this.options.selector.query).find("input, textarea, [contenteditable], .typeahead__hint").css({paddingLeft:t,paddingTop:e})},showLayout:function(){!this.container.hasClass("result")&&(this.result.length||this.displayEmptyTemplate||this.options.backdropOnFocus)&&(function(){var e=this;A("html").off("keydown"+this.namespace).on("keydown"+this.namespace,function(t){t.keyCode&&9===t.keyCode&&setTimeout(function(){A(":focus").closest(e.container).find(e.node)[0]||e.hideLayout()},0)}),A("html").off("click"+this.namespace+" touchend"+this.namespace).on("click"+this.namespace+" touchend"+this.namespace,function(t){A(t.target).closest(e.container)[0]||A(t.target).closest("."+e.options.selector.item)[0]||t.target.className===e.options.selector.cancelButton||e.hasDragged||e.hideLayout()})}.call(this),this.container.addClass([this.result.length||this.searchGroups.length&&this.displayEmptyTemplate?"result ":"",this.options.hint&&this.searchGroups.length?"hint":"",this.options.backdrop||this.options.backdropOnFocus?"backdrop":""].join(" ")),this.helper.executeCallback.call(this,this.options.callback.onShowLayout,[this.node,this.query]))},hideLayout:function(){(this.container.hasClass("result")||this.container.hasClass("backdrop"))&&(this.container.removeClass("result hint filter"+(this.options.backdropOnFocus&&A(this.node).is(":focus")?"":" backdrop")),this.options.backdropOnFocus&&this.container.hasClass("backdrop")||(A("html").off(this.namespace),this.helper.executeCallback.call(this,this.options.callback.onHideLayout,[this.node,this.query])))},resetLayout:function(){this.result=[],this.tmpResult={},this.groups=[],this.resultCount=0,this.resultCountPerGroup={},this.resultItemCount=0,this.resultHtml=null,this.options.hint&&this.hint.container&&(this.hint.container.val(""),this.isContentEditable)&&this.hint.container.text("")},resetInput:function(){this.node.val(""),this.isContentEditable&&this.node.text(""),this.query="",this.rawQuery=""},buildCancelButtonLayout:function(){var e;this.options.cancelButton&&A("<span/>",{class:(e=this).options.selector.cancelButton,html:"×",mousedown:function(t){t.stopImmediatePropagation(),t.preventDefault(),e.resetInput(),e.node.trigger("input"+e.namespace,[t])}}).insertBefore(this.node)},toggleCancelButtonVisibility:function(){this.container.toggleClass("cancel",!!this.query.length)},__construct:function(){this.extendOptions(),this.unifySourceFormat()&&(this.dynamicFilter.init.apply(this),this.init(),this.buildDropdownLayout(),this.buildDropdownItemLayout("static"),this.buildMultiselectLayout(),this.delegateEvents(),this.buildCancelButtonLayout(),this.helper.executeCallback.call(this,this.options.callback.onReady,[this.node]))},helper:{isEmpty:function(t){for(var e in t)if(t.hasOwnProperty(e))return!1;return!0},removeAccent:function(t){var e;if("string"==typeof t)return e=o,"object"==typeof this.options.accent&&(e=this.options.accent),t.toLowerCase().replace(new RegExp("["+e.from+"]","g"),function(t){return e.to[e.from.indexOf(t)]})},slugify:function(t){return t=""!==(t=String(t))?(t=this.helper.removeAccent.call(this,t)).replace(/[^-a-z0-9]+/g,"-").replace(/-+/g,"-").replace(/^-|-$/g,""):t},sort:function(n,i,o){function s(t){for(var e=0,i=n.length;e<i;e++)if(void 0!==t[n[e]])return o(t[n[e]]);return t}return i=[-1,1][+!!i],function(t,e){return t=s(t),e=s(e),i*((e<t)-(t<e))}},replaceAt:function(t,e,i,n){return t.substring(0,e)+n+t.substring(e+i)},highlight:function(t,e,i){t=String(t);var i=i&&this.helper.removeAccent.call(this,t)||t,n=[];(e=Array.isArray(e)?e:[e]).sort(function(t,e){return e.length-t.length});for(var o=e.length-1;0<=o;o--)""!==e[o].trim()?e[o]=e[o].replace(/[-[\]{}()*+?.,\\^$|#\s]/g,"\\$&"):e.splice(o,1);for(i.replace(new RegExp("(?:"+e.join("|")+")(?!([^<]+)?>)","gi"),function(t,e,i){n.push({offset:i,length:t.length})}),o=n.length-1;0<=o;o--)t=this.helper.replaceAt(t,n[o].offset,n[o].length,"<strong>"+t.substr(n[o].offset,n[o].length)+"</strong>");return t},getCaret:function(t){var e=0;if(t.selectionStart)return t.selectionStart;if(document.selection){var i=document.selection.createRange();if(null===i)return e;var n=t.createTextRange(),o=n.duplicate();n.moveToBookmark(i.getBookmark()),o.setEndPoint("EndToStart",n),e=o.text.length}else window.getSelection&&(i=window.getSelection()).rangeCount&&(n=i.getRangeAt(0)).commonAncestorContainer.parentNode==t&&(e=n.endOffset);return e},setCaretAtEnd:function(t){var e,i;void 0!==window.getSelection&&void 0!==document.createRange?((e=document.createRange()).selectNodeContents(t),e.collapse(!1),(i=window.getSelection()).removeAllRanges(),i.addRange(e)):void 0!==document.body.createTextRange&&((i=document.body.createTextRange()).moveToElementText(t),i.collapse(!1),i.select())},cleanStringFromScript:function(t){return"string"==typeof t&&t.replace(/<\/?(?:script|iframe)\b[^>]*>/gm,"")||t},executeCallback:function(t,e){if(t){var i;if("function"==typeof t)i=t;else if(("string"==typeof t||Array.isArray(t))&&"function"!=typeof(i=this.helper.namespace.call(this,(t="string"==typeof t?[t,[]]:t)[0],window)))return;return i.apply(this,(t[1]||[]).concat(e||[]))}},namespace:function(t,e,i,n){if("string"!=typeof t||""===t)return!1;var o=void 0!==n?n:void 0;if(!~t.indexOf("."))return e[t]||o;for(var s,r=t.split("."),a=e||window,l=(i=i||"get",0),c=r.length;l<c;l++){if(void 0===a[s=r[l]]){if(~["get","delete"].indexOf(i))return void 0!==n?n:void 0;a[s]={}}if(~["set","create","delete"].indexOf(i)&&l===c-1){if("set"!==i&&"create"!==i)return delete a[s],!0;a[s]=o}a=a[s]}return a},typeWatch:(i=0,function(t,e){clearTimeout(i),i=setTimeout(t,e)})}},A.fn.typeahead=A.typeahead=function(t){return e.typeahead(this,t)},{typeahead:function(t,e){if(e&&e.source&&"object"==typeof e.source){if("function"==typeof t){if(!e.input)return;t=A(e.input)}if(t.length){if(void 0===t[0].value&&(t[0].value=t.text()),1===t.length)return t[0].selector=t.selector||e.input||t[0].nodeName.toLowerCase(),window.Typeahead[t[0].selector]=new r(t,e);for(var i,n={},o=0,s=t.length;o<s;++o)void 0!==n[i=t[o].nodeName.toLowerCase()]&&(i+=o),t[o].selector=i,window.Typeahead[i]=n[i]=new r(t.eq(o),e);return n}}}});return window.console=window.console||{log:function(){}},Array.isArray||(Array.isArray=function(t){return"[object Array]"===Object.prototype.toString.call(t)}),"trim"in String.prototype||(String.prototype.trim=function(){return this.replace(/^\s+/,"").replace(/\s+$/,"")}),"indexOf"in Array.prototype||(Array.prototype.indexOf=function(t,e){(e=void 0===e?0:e)<0&&(e+=this.length),e<0&&(e=0);for(var i=this.length;e<i;e++)if(e in this&&this[e]===t)return e;return-1}),Object.keys||(Object.keys=function(t){var e,i=[];for(e in t)Object.prototype.hasOwnProperty.call(t,e)&&i.push(e);return i}),r}),!function(r,h){function t(i){return"object"!=typeof i||!i||"length"in i||w(i).forEach(function(t){if(/^(?:forceHandCursor|title|zIndex|bubbleEvents|fixLineEndings)$/.test(t))A[t]=i[t];else if(null==T.bridge){if(!("containerId"!==t&&"swfObjectId"!==t||"string"==typeof(e=i[t])&&e&&/^[A-Za-z][A-Za-z0-9_:\-\.]*$/.test(e)))throw new Error("The specified `"+t+"` value is not valid as an HTML4 Element ID");A[t]=i[t]}var e}),"string"==typeof i&&i?_.call(A,i)?A[i]:void 0:s(A)}function c(){return!!(m.addEventListener&&f.Object.keys&&f.Array.prototype.map)}function i(t){var e,i;if((t=W(t))&&!function(t){var e=t.target||l||null,i=t._source==="swf";switch(delete t._source,t.type){case"error":var n=t.name==="flash-sandboxed"||kt(t);if(typeof n==="boolean")T.sandboxed=n;if(t.name==="browser-unsupported")x(T,{disabled:false,outdated:false,unavailable:false,degraded:false,deactivated:false,overdue:false,ready:false});else if(D.indexOf(t.name)!==-1)x(T,{disabled:t.name==="flash-disabled",outdated:t.name==="flash-outdated",insecure:t.name==="flash-insecure",unavailable:t.name==="flash-unavailable",degraded:t.name==="flash-degraded",deactivated:t.name==="flash-deactivated",overdue:t.name==="flash-overdue",ready:false});else if(t.name==="version-mismatch"){u=t.swfVersion;x(T,{disabled:false,outdated:false,insecure:false,unavailable:false,degraded:false,deactivated:false,overdue:false,ready:false})}j();break;case"ready":u=t.swfVersion;var o=T.deactivated===true;x(T,{sandboxed:false,disabled:false,outdated:false,insecure:false,unavailable:false,degraded:false,deactivated:false,overdue:o,ready:!o});j();break;case"beforecopy":p=e;break;case"copy":var s,r,a=t.relatedTarget;if(!(C["text/html"]||C["text/plain"])&&a&&(r=a.value||a.outerHTML||a.innerHTML)&&(s=a.value||a.textContent||a.innerText)){t.clipboardData.clearData();t.clipboardData.setData("text/plain",s);if(r!==s)t.clipboardData.setData("text/html",r)}else if(!C["text/plain"]&&t.target&&(s=t.target.getAttribute("data-clipboard-text"))){t.clipboardData.clearData();t.clipboardData.setData("text/plain",s)}break;case"aftercopy":xt(t);P.clearData();if(e&&e!==Ot()&&e.focus)e.focus();break;case"_mouseover":P.focus(e);if(A.bubbleEvents===true&&i){if(e&&e!==t.relatedTarget&&!rt(t.relatedTarget,e))L(x({},t,{type:"mouseenter",bubbles:false,cancelable:false}));L(x({},t,{type:"mouseover"}))}break;case"_mouseout":P.blur();if(A.bubbleEvents===true&&i){if(e&&e!==t.relatedTarget&&!rt(t.relatedTarget,e))L(x({},t,{type:"mouseleave",bubbles:false,cancelable:false}));L(x({},t,{type:"mouseout"}))}break;case"_mousedown":$t(e,A.activeClass);if(A.bubbleEvents===true&&i)L(x({},t,{type:t.type.slice(1)}));break;case"_mouseup":$(e,A.activeClass);if(A.bubbleEvents===true&&i)L(x({},t,{type:t.type.slice(1)}));break;case"_click":p=null;if(A.bubbleEvents===true&&i)L(x({},t,{type:t.type.slice(1)}));break;case"_mousemove":if(A.bubbleEvents===true&&i)L(x({},t,{type:t.type.slice(1)}));break}if(/^_(?:click|mouse(?:over|out|down|up|move))$/.test(t.type))return true}(t))return"ready"===t.type&&!0===T.overdue?P.emit({type:"error",name:"flash-overdue"}):(i=x({},t),function(t){if("object"==typeof t&&t&&t.type){var e=wt(t),i=S["*"]||[],n=S[t.type]||[],o=i.concat(n);if(o&&o.length)for(var s,r,a,l,c,d=this,s=0,r=o.length;s<r;s++)l=this,"function"==typeof(a="object"==typeof(a="string"==typeof(a=o[s])&&"function"==typeof f[a]?f[a]:a)&&a&&"function"==typeof a.handleEvent?(l=a).handleEvent:a)&&(c=x({},t),_t(a,l,[c],e));return this}}.call(this,i),"copy"===t.type&&(e=(i=function(t){var e={},i={};if("object"==typeof t&&t){for(var n in t)if(n&&_.call(t,n)&&"string"==typeof t[n]&&t[n])switch(n.toLowerCase()){case"text/plain":case"text":case"air:text":case"flash:text":e.text=t[n],i.text=n;break;case"text/html":case"html":case"air:html":case"flash:html":e.html=t[n],i.html=n;break;case"application/rtf":case"text/rtf":case"rtf":case"richtext":case"air:rtf":case"flash:rtf":e.rtf=t[n],i.rtf=n}return{data:e,formatMap:i}}}(C)).data,n=i.formatMap),e)}function H(){var t=T.sandboxed;if(c()){if(d(),"boolean"!=typeof T.ready&&(T.ready=!1),T.sandboxed!==t&&!0===T.sandboxed)T.ready=!1,P.emit({type:"error",name:"flash-sandboxed"});else if(!P.isFlashUnusable()&&null===T.bridge)if((t=yt())&&t!==f.location.protocol)P.emit({type:"error",name:"flash-insecure"});else{"number"==typeof(t=A.flashLoadTimeout)&&0<=t&&(E=y(function(){"boolean"!=typeof T.deactivated&&(T.deactivated=!0),!0===T.deactivated&&P.emit({type:"error",name:"flash-deactivated"})},t)),T.overdue=!1;var t=T.bridge,e=O(t);if(!t){var i=Lt(f.location.host,A);var n=i==="never"?"none":"all";var o=At(x({jsVersion:P.version},A));var s=A.swfPath+Dt(A.swfPath,A);if(ut)s=Ct(s);e=St();var r=m.createElement("div");e.appendChild(r);m.body.appendChild(e);var a=m.createElement("div");var l=T.pluginType==="activex";a.innerHTML='<object id="'+A.swfObjectId+'" name="'+A.swfObjectId+'" '+'width="100%" height="100%" '+(l?'classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"':'type="application/x-shockwave-flash" data="'+s+'"')+">"+(l?'<param name="movie" value="'+s+'"/>':"")+'<param name="allowScriptAccess" value="'+i+'"/>'+'<param name="allowNetworking" value="'+n+'"/>'+'<param name="menu" value="false"/>'+'<param name="wmode" value="transparent"/>'+'<param name="flashvars" value="'+o+'"/>'+'<div id="'+A.swfObjectId+'_fallbackContent"> </div>'+"</object>";t=a.firstChild;a=null;st(t).ZeroClipboard=P;e.replaceChild(t,r);Tt()}if(!t){t=m[A.swfObjectId];if(t&&(l=t.length))t=t[l-1];if(!t&&e)t=e.firstChild}T.bridge=t||null}}else T.ready=!1,P.emit({type:"error",name:"browser-unsupported"})}function F(){P.clearData(),P.blur(),P.emit("destroy");var i,n=T.bridge;n&&((i=O(n))&&("activex"===T.pluginType&&"readyState"in n?(n.style.display="none",function t(){if(4===n.readyState){for(var e in n)"function"==typeof n[e]&&(n[e]=null);n.parentNode&&n.parentNode.removeChild(n),i.parentNode&&i.parentNode.removeChild(i)}else y(t,10)}()):(n.parentNode&&n.parentNode.removeChild(n),i.parentNode&&i.parentNode.removeChild(i))),j(),T.ready=null,T.bridge=null,T.deactivated=null,T.insecure=null,u=h),P.off()}function R(t,e){var i,n;if("object"==typeof t&&t&&void 0===e)i=t,P.clearData();else{if("string"!=typeof t||!t)return;(i={})[t]=e}for(n in i)"string"==typeof n&&n&&_.call(i,n)&&"string"==typeof i[n]&&i[n]&&(C[n]=function(t){var e=/(\r\n|\r|\n)/g;if(typeof t==="string"&&A.fixLineEndings===true)if(ct()){if(/((^|[^\r])\n|\r([^\n]|$))/.test(t))t=t.replace(e,"\r\n")}else if(/\r/.test(t))t=t.replace(e,"\n");return t}(i[n]))}function q(t){if(void 0===t){var e=C;if(e)for(var i in e)_.call(e,i)&&delete e[i];n=null}else"string"==typeof t&&_.call(C,t)&&delete C[t]}function Y(t){if(t&&1===t.nodeType){l&&($(l,A.activeClass),l!==t)&&$(l,A.hoverClass),$t(l=t,A.hoverClass);var e=t.getAttribute("title")||A.title;"string"==typeof e&&e&&(i=O(T.bridge))&&i.setAttribute("title",e);var i=!0===A.forceHandCursor||"pointer"===Mt(t,"cursor");if(T.ready===true)if(T.bridge&&typeof T.bridge.setHandCursor==="function")T.bridge.setHandCursor(i);else T.ready=false;if(l&&(e=O(T.bridge))){t=M(l);x(e.style,{width:t.width+"px",height:t.height+"px",top:t.top+"px",left:t.left+"px",zIndex:""+o(A.zIndex)})}}}function W(t){var e;if("string"==typeof t&&t?(e=t,t={}):"object"==typeof t&&t&&"string"==typeof t.type&&t.type&&(e=t.type),e)return e=e.toLowerCase(),!t.target&&(/^(copy|aftercopy|_click)$/.test(e)||"error"===e&&"clipboard-error"===t.name)&&(t.target=p),x(t,{type:e,target:t.target||l||null,relatedTarget:t.relatedTarget||null,currentTarget:T&&T.bridge||null,timeStamp:t.timeStamp||nt()||null}),e=pt[t.type],(e="error"===t.type&&t.name?e&&e[t.name]:e)&&(t.message=e),"ready"===t.type&&x(t,{target:null,version:T.version}),"error"===t.type&&(mt.test(t.name)&&x(t,{target:null,minimumVersion:ht}),gt.test(t.name)&&x(t,{version:T.version}),"flash-insecure"===t.name)&&x(t,{pageProtocol:f.location.protocol,swfProtocol:yt()}),"copy"===t.type&&(t.clipboardData={setData:P.setData,clearData:P.clearData}),(t="aftercopy"===t.type?Et(t,n):t).target&&!t.relatedTarget&&(t.relatedTarget=vt(t.target)),bt(t)}function a(t){var e,i;return null!=t&&""!==t&&""!==(t=t.replace(/^\s+|\s+$/g,""))&&(!(t=-1===(i=(t=-1===(e=t.indexOf("//"))?t:t.slice(e+2)).indexOf("/"))?t:-1===e||0===i?null:t.slice(0,i))||".swf"!==t.slice(-4).toLowerCase())&&t||null}function d(t){var e,i,n,o=T.sandboxed,s=null;if(t=!0===t,!1==dt)s=!1;else{try{i=r.frameElement||null}catch(t){n={name:t.name,message:t.message}}if(i&&1===i.nodeType&&"IFRAME"===i.nodeName)try{s=i.hasAttribute("sandbox")}catch(t){s=null}else{try{e=document.domain||null}catch(t){e=null}(null===e||n&&"SecurityError"===n.name&&/(^|[\s\(\[@])sandbox(es|ed|ing|[\s\.,!\)\]@]|$)/.test(n.message.toLowerCase()))&&(s=!0)}}o===(T.sandboxed=s)||t||B(K)}function B(e){var i,t,n=!1,o=!1,s=!1,r="";function a(t){t=t.match(/[\d]+/g);return t.length=3,t.join(".")}function l(t){t&&(n=!0,!(r=t.version?a(t.version):r)&&t.description&&(r=a(t.description)),t.filename)&&(t=t.filename,s=!!t&&(t=t.toLowerCase())&&(/^(pepflashplayer\.dll|libpepflashplayer\.so|pepperflashplayer\.plugin)$/.test(t)||"chrome.plugin"===t.slice(-13)))}if(g.plugins&&g.plugins.length)l(g.plugins["Shockwave Flash"]),g.plugins["Shockwave Flash 2.0"]&&(n=!0,r="2.0.0.11");else if(g.mimeTypes&&g.mimeTypes.length)l((t=g.mimeTypes["application/x-shockwave-flash"])&&t.enabledPlugin);else if(void 0!==e){o=!0;try{i=new e("ShockwaveFlash.ShockwaveFlash.7"),n=!0,r=a(i.GetVariable("$version"))}catch(t){try{i=new e("ShockwaveFlash.ShockwaveFlash.6"),n=!0,r="6.0.21"}catch(t){try{i=new e("ShockwaveFlash.ShockwaveFlash"),n=!0,r=a(i.GetVariable("$version"))}catch(t){o=!1}}}}T.disabled=!0!==n,T.outdated=r&&b(r)<b(ht),T.version=r||"0.0.0",T.pluginType=s?"pepper":o?"activex":n?"netscape":"unknown"}function z(t){return function(t){if(!(t&&t.type))return false;if(t.client&&t.client!==this)return false;var e=N[this.id],i=e&&e.elements,n=!!i&&i.length>0,o=!t.target||n&&i.indexOf(t.target)!==-1,s=t.relatedTarget&&n&&i.indexOf(t.relatedTarget)!==-1,r=t.client&&t.client===this;return e&&(o||s||r)?!0:!1}.call(this,t)&&("object"==typeof t&&t&&"string"==typeof t.type&&t.type&&(t=x({},t)),t=x({},W(t),{client:this}),function(t){var e=N[this.id];if("object"==typeof t&&t&&t.type&&e){var i=wt(t),n=e&&e.handlers["*"]||[],o=e&&e.handlers[t.type]||[],s=n.concat(o);if(s&&s.length)for(var r,a,l,c,d,u=this,r=0,a=s.length;r<a;r++)c=this,"function"==typeof(l="object"==typeof(l="string"==typeof(l=s[r])&&"function"==typeof f[l]?f[l]:l)&&l&&"function"==typeof l.handleEvent?(c=l).handleEvent:l)&&(d=x({},t),_t(l,c,[d],i))}}.call(this,t)),this}function U(t){if(!N[this.id])throw new Error("Attempted to clip element(s) to a destroyed ZeroClipboard client instance");t=V(t);for(var e,i=0;i<t.length;i++)_.call(t,i)&&t[i]&&1===t[i].nodeType&&(t[i].zcClippingId?-1===I[t[i].zcClippingId].indexOf(this.id)&&I[t[i].zcClippingId].push(this.id):(t[i].zcClippingId="zcClippingId_"+Pt++,I[t[i].zcClippingId]=[this.id],!0===A.autoActivate&&function(e){var i,t;e&&1===e.nodeType&&(e.addEventListener("mouseover",t=function(t){(t=t||f.event)&&(i(t),P.focus(e))},!(i=function(t){(t=t||f.event)&&("js"!==t._source&&(t.stopImmediatePropagation(),t.preventDefault()),delete t._source)})),e.addEventListener("mouseout",i,!1),e.addEventListener("mouseenter",i,!1),e.addEventListener("mouseleave",i,!1),e.addEventListener("mousemove",i,!1),Nt[e.zcClippingId]={mouseover:t,mouseout:i,mouseenter:i,mouseleave:i,mousemove:i})}(t[i])),-1===(e=N[this.id]&&N[this.id].elements).indexOf(t[i]))&&e.push(t[i]);return this}function G(t){var e=N[this.id];if(e)for(var i,n=e.elements,o=(t=void 0===t?n.slice(0):V(t)).length;o--;)if(_.call(t,o)&&t[o]&&1===t[o].nodeType){for(i=0;-1!==(i=n.indexOf(t[o],i));)n.splice(i,1);var s=I[t[o].zcClippingId];if(s){for(i=0;-1!==(i=s.indexOf(this.id,i));)s.splice(i,1);if(0===s.length){if(!0===A.autoActivate){h=u=d=c=l=a=void 0;var r=t[o];if(r&&1===r.nodeType){var a=Nt[r.zcClippingId];if("object"==typeof a&&a){for(var l,c,d=["move","leave","enter","out","over"],u=0,h=d.length;u<h;u++)"function"==typeof(c=a[l="mouse"+d[u]])&&r.removeEventListener(l,c,!1);delete Nt[r.zcClippingId]}}}delete t[o].zcClippingId}}}return this}function V(t){return"number"!=typeof(t="string"==typeof t?[]:t).length?[t]:t}var u,l,p,f=r,m=f.document,g=f.navigator,y=f.setTimeout,X=f.clearTimeout,Z=f.setInterval,Q=f.clearInterval,J=f.getComputedStyle,v=f.encodeURIComponent,K=f.ActiveXObject,tt=f.Error,et=f.Number.parseInt||f.parseInt,b=f.Number.parseFloat||f.parseFloat,it=f.Number.isNaN||f.isNaN,nt=f.Date.now,w=f.Object.keys,_=f.Object.prototype.hasOwnProperty,ot=f.Array.prototype.slice,st=function(){var t=function(t){return t};if("function"==typeof f.wrap&&"function"==typeof f.unwrap)try{var e=m.createElement("div"),i=f.unwrap(e);1===e.nodeType&&i&&1===i.nodeType&&(t=f.unwrap)}catch(t){}return t}(),k=function(t){return ot.call(t,0)},x=function(){for(var t,e,i,n=k(arguments),o=n[0]||{},s=1,r=n.length;s<r;s++)if(null!=(t=n[s]))for(e in t)_.call(t,e)&&(o[e],o!==(i=t[e]))&&i!==h&&(o[e]=i);return o},s=function(t){var e,i,n,o;if("object"!=typeof t||null==t||"number"==typeof t.nodeType)e=t;else if("number"==typeof t.length)for(e=[],i=0,n=t.length;i<n;i++)_.call(t,i)&&(e[i]=s(t[i]));else for(o in e={},t)_.call(t,o)&&(e[o]=s(t[o]));return e},rt=function(t,e){if(t&&1===t.nodeType&&t.ownerDocument&&e&&(1===e.nodeType&&e.ownerDocument&&e.ownerDocument===t.ownerDocument||9===e.nodeType&&!e.ownerDocument&&e===t.ownerDocument))do{if(t===e)return!0}while(t=t.parentNode);return!1},at=function(t){var e;return"string"==typeof t&&t&&(e=t.split("#")[0].split("?")[0],e=t.slice(0,t.lastIndexOf("/")+1)),e},lt=function(){var t,e,i,n;try{throw new tt}catch(t){e=t}return t=e?e.sourceURL||e.fileName||(e=e.stack,i="string"==typeof e&&e&&((n=e.match(/^(?:|[^:@]*@|.+\)@(?=http[s]?|file)|.+?\s+(?: at |@)(?:[^:\(]+ )*[\(]?)((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/))&&n[1]||(n=e.match(/\)@((?:http[s]?|file):\/\/[\/]?.+?\/[^:\)]*?)(?::\d+)(?::\d+)?/))&&n[1])?n[1]:i):t},ct=function(){var t=/win(dows|[\s]?(nt|me|ce|xp|vista|[\d]+))/i;return!!g&&(t.test(g.appVersion||"")||t.test(g.platform||"")||-1!==(g.userAgent||"").indexOf("Windows"))},dt=null==f.opener&&(!!f.top&&f!=f.top||!!f.parent&&f!=f.parent),ut="html"===m.documentElement.nodeName,T={bridge:null,version:"0.0.0",pluginType:"unknown",sandboxed:null,disabled:null,outdated:null,insecure:null,unavailable:null,degraded:null,deactivated:null,overdue:null,ready:null},ht="11.0.0",S={},C={},n=null,E=0,e=0,pt={ready:"Flash communication is established",error:{"flash-sandboxed":"Attempting to run Flash in a sandboxed iframe, which is impossible","flash-disabled":"Flash is disabled or not installed. May also be attempting to run Flash in a sandboxed iframe, which is impossible.","flash-outdated":"Flash is too outdated to support ZeroClipboard","flash-insecure":"Flash will be unable to communicate due to a protocol mismatch between your `swfPath` configuration and the page","flash-unavailable":"Flash is unable to communicate bidirectionally with JavaScript","flash-degraded":"Flash is unable to preserve data fidelity when communicating with JavaScript","flash-deactivated":"Flash is too outdated for your browser and/or is configured as click-to-activate.\nThis may also mean that the ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity.\nMay also be attempting to run Flash in a sandboxed iframe, which is impossible.","flash-overdue":"Flash communication was established but NOT within the acceptable time limit","version-mismatch":"ZeroClipboard JS version number does not match ZeroClipboard SWF version number","clipboard-error":"At least one error was thrown while ZeroClipboard was attempting to inject your data into the clipboard","config-mismatch":"ZeroClipboard configuration does not match Flash's reality","swf-not-found":"The ZeroClipboard SWF object could not be loaded, so please check your `swfPath` configuration and/or network connectivity","browser-unsupported":"The browser does not support the required HTML DOM and JavaScript features"}},ft=["flash-unavailable","flash-degraded","flash-overdue","version-mismatch","config-mismatch","clipboard-error"],D=["flash-sandboxed","flash-disabled","flash-outdated","flash-insecure","flash-unavailable","flash-degraded","flash-deactivated","flash-overdue"],mt=new RegExp("^flash-("+D.map(function(t){return t.replace(/^flash-/,"")}).join("|")+")$"),gt=new RegExp("^flash-("+D.filter(function(t){return"flash-disabled"!==t}).map(function(t){return t.replace(/^flash-/,"")}).join("|")+")$"),A={swfPath:(at(function(){var t,e,i;if(m.currentScript&&(t=m.currentScript.src))return t;if(1===(e=m.getElementsByTagName("script")).length)return e[0].src||h;if("readyState"in(e[0]||document.createElement("script")))for(i=e.length;i--;)if("interactive"===e[i].readyState&&(t=e[i].src))return t;return"loading"===m.readyState&&(t=e[e.length-1].src)?t:(t=lt())||h}())||function(){for(var t,e,i=m.getElementsByTagName("script"),n=i.length;n--;){if(!(e=i[n].src)){t=null;break}if(e=at(e),null==t)t=e;else if(t!==e){t=null;break}}return t||h}()||"")+"ZeroClipboard.swf",trustedDomains:f.location.host?[f.location.host]:[],cacheBust:!0,forceEnhancedClipboard:!1,flashLoadTimeout:3e4,autoActivate:!0,bubbleEvents:!0,fixLineEndings:!0,containerId:"global-zeroclipboard-html-bridge",containerClass:"global-zeroclipboard-container",swfObjectId:"global-zeroclipboard-flash-bridge",hoverClass:!1,activeClass:!1,forceHandCursor:!1,title:null,zIndex:999999999},yt=function(){var t=A.swfPath||"",e=t.slice(0,2),t=t.slice(0,t.indexOf("://")+1);return"\\\\"===e?"file:":"//"===e||""===t?f.location.protocol:t},vt=function(t){t=t&&t.getAttribute&&t.getAttribute("data-clipboard-target");return t?m.getElementById(t):null},bt=function(t){var e,i,n,o,s,r,a,l,c,d,u;return t&&/^_(?:click|mouse(?:over|out|down|up|move))$/.test(t.type)&&(e=t.target,i="_mouseover"===t.type&&t.relatedTarget?t.relatedTarget:h,n="_mouseout"===t.type&&t.relatedTarget?t.relatedTarget:h,a=M(e),o=f.screenLeft||f.screenX||0,s=f.screenTop||f.screenY||0,r=m.body.scrollLeft+m.documentElement.scrollLeft,c=m.body.scrollTop+m.documentElement.scrollTop,l=o+(r=(o=a.left+("number"==typeof t._stageX?t._stageX:0))-r),c=s+(a=(s=a.top+("number"==typeof t._stageY?t._stageY:0))-c),d="number"==typeof t.movementX?t.movementX:0,u="number"==typeof t.movementY?t.movementY:0,delete t._stageX,delete t._stageY,x(t,{srcElement:e,fromElement:i,toElement:n,screenX:l,screenY:c,pageX:o,pageY:s,clientX:r,clientY:a,x:r,y:a,movementX:d,movementY:u,offsetX:0,offsetY:0,layerX:0,layerY:0})),t},wt=function(t){t=t&&"string"==typeof t.type&&t.type||"";return!/^(?:(?:before)?copy|destroy)$/.test(t)},_t=function(t,e,i,n){n?y(function(){t.apply(e,i)},0):t.apply(e,i)},kt=function(t){var e=null;return e=!1==dt||t&&"error"===t.type&&t.name&&-1!==ft.indexOf(t.name)?!1:e},xt=function(t){var e;t.errors&&0<t.errors.length&&(e=s(t),x(e,{type:"error",name:"clipboard-error"}),delete e.success,y(function(){P.emit(e)},0))},L=function(t){var e,i,n;t&&"string"==typeof t.type&&t&&(n={view:(i=(e=t.target||null)&&e.ownerDocument||m).defaultView||f,canBubble:!0,cancelable:!0,detail:"click"===t.type?1:0,button:"number"==typeof t.which?t.which-1:"number"==typeof t.button?t.button:i.createEvent?0:1},n=x(n,t),e)&&i.createEvent&&e.dispatchEvent&&(n=[n.type,n.canBubble,n.cancelable,n.view,n.detail,n.screenX,n.screenY,n.clientX,n.clientY,n.ctrlKey,n.altKey,n.shiftKey,n.metaKey,n.button,n.relatedTarget],(t=i.createEvent("MouseEvents")).initMouseEvent)&&(t.initMouseEvent.apply(t,n),t._source="js",e.dispatchEvent(t))},Tt=function(){var a,t=A.flashLoadTimeout;"number"==typeof t&&0<=t&&(t=Math.min(1e3,t/10),a=A.swfObjectId+"_fallbackContent",e=Z(function(){var t,e,i,n,o,s,r=m.getElementById(a);(r=r)&&!!(t=J(r,null))&&(e=0<b(t.height),i=0<b(t.width),n=0<=b(t.top),o=0<=b(t.left),r=(s=e&&i&&n&&o)?null:M(r),"none"!==t.display)&&"collapse"!==t.visibility&&(s||!!r&&(e||0<r.height)&&(i||0<r.width)&&(n||0<=r.top)&&(o||0<=r.left))&&(j(),T.deactivated=null,P.emit({type:"error",name:"swf-not-found"}))},t))},St=function(){var t=m.createElement("div");return t.id=A.containerId,t.className=A.containerClass,t.style.position="absolute",t.style.left="0px",t.style.top="-9999px",t.style.width="1px",t.style.height="1px",t.style.zIndex=""+o(A.zIndex),t},O=function(t){for(var e=t&&t.parentNode;e&&"OBJECT"===e.nodeName&&e.parentNode;)e=e.parentNode;return e||null},Ct=function(t){return"string"==typeof t&&t?t.replace(/["&'<>]/g,function(t){switch(t){case'"':return""";case"&":return"&";case"'":return"'";case"<":return"<";case">":return">";default:return t}}):t},Et=function(t,e){if("object"!=typeof t||!t||"object"!=typeof e||!e)return t;var i,n={};for(i in t)if(_.call(t,i))if("errors"===i){n[i]=t[i]?t[i].slice():[];for(var o=0,s=n[i].length;o<s;o++)n[i][o].format=e[n[i][o].format]}else if("success"!==i&&"data"!==i)n[i]=t[i];else{n[i]={};var r,a=t[i];for(r in a)r&&_.call(a,r)&&_.call(e,r)&&(n[i][e[r]]=a[r])}return n},Dt=function(t,e){return null==e||e&&!0===e.cacheBust?(-1===t.indexOf("?")?"?":"&")+"noCache="+nt():""},At=function(t){var e,i,n,o,s="",r=[];if(t.trustedDomains&&("string"==typeof t.trustedDomains?o=[t.trustedDomains]:"object"==typeof t.trustedDomains&&"length"in t.trustedDomains&&(o=t.trustedDomains)),o&&o.length)for(e=0,i=o.length;e<i;e++)if(_.call(o,e)&&o[e]&&"string"==typeof o[e]&&(n=a(o[e]))){if("*"===n){r.length=0,r.push(n);break}r.push.apply(r,[n,"//"+n,f.location.protocol+"//"+n])}return r.length&&(s+="trustedOrigins="+v(r.join(","))),!0===t.forceEnhancedClipboard&&(s+=(s?"&":"")+"forceEnhancedClipboard=true"),"string"==typeof t.swfObjectId&&t.swfObjectId&&(s+=(s?"&":"")+"swfObjectId="+v(t.swfObjectId)),"string"==typeof t.jsVersion&&t.jsVersion&&(s+=(s?"&":"")+"jsVersion="+v(t.jsVersion)),s},Lt=function(t,e){var i=a(e.swfPath),e=(null===i&&(i=t),function(t){var e,i,n,o=[];if("object"==typeof(t="string"==typeof t?[t]:t)&&t&&"number"==typeof t.length)for(e=0,i=t.length;e<i;e++)if(_.call(t,e)&&(n=a(t[e]))){if("*"===n){o.length=0,o.push("*");break}-1===o.indexOf(n)&&o.push(n)}return o}(e.trustedDomains)),n=e.length;if(0<n){if(1===n&&"*"===e[0])return"always";if(-1!==e.indexOf(t))return 1===n&&t===i?"sameDomain":"always"}return"never"},Ot=function(){try{return m.activeElement}catch(t){return null}},$t=function(t,e){var i,n,o,s=[];if("string"==typeof e&&e&&(s=e.split(/\s+/)),t&&1===t.nodeType&&0<s.length){for(o=(" "+(t.className||"")+" ").replace(/[\t\r\n\f]/g," "),i=0,n=s.length;i<n;i++)-1===o.indexOf(" "+s[i]+" ")&&(o+=s[i]+" ");(o=o.replace(/^\s+|\s+$/g,""))!==t.className&&(t.className=o)}return t},$=function(t,e){var i,n,o,s=[];if("string"==typeof e&&e&&(s=e.split(/\s+/)),t&&1===t.nodeType&&0<s.length&&t.className){for(o=(" "+t.className+" ").replace(/[\t\r\n\f]/g," "),i=0,n=s.length;i<n;i++)o=o.replace(" "+s[i]+" "," ");(o=o.replace(/^\s+|\s+$/g,""))!==t.className&&(t.className=o)}return t},Mt=function(t,e){var i=J(t,null).getPropertyValue(e);return"cursor"!==e||i&&"auto"!==i||"A"!==t.nodeName?i:"pointer"},M=function(t){var e,i,n,o,s,r,a,l,c={left:0,top:0,width:0,height:0};return t.getBoundingClientRect&&(t=t.getBoundingClientRect(),e=f.pageXOffset,i=f.pageYOffset,n=m.documentElement.clientLeft||0,o=m.documentElement.clientTop||0,l=a=0,"relative"===Mt(m.body,"position")&&(s=m.body.getBoundingClientRect(),r=m.documentElement.getBoundingClientRect(),a=s.left-r.left||0,l=s.top-r.top||0),c.left=t.left+e-n-a,c.top=t.top+i-o-l,c.width="width"in t?t.width:t.right-t.left,c.height="height"in t?t.height:t.bottom-t.top),c},j=function(){X(E),E=0,Q(e),e=0},o=function(t){var e;return/^(?:auto|inherit)$/.test(t)?t:("number"!=typeof t||it(t)?"string"==typeof t&&(e=o(et(t,10))):e=t,"number"==typeof e?e:"auto")},P=(B(K),d(!0),function(){if(!(this instanceof P))return new P;"function"==typeof P._createClient&&P._createClient.apply(this,k(arguments))}),jt=(P.version="2.4.0-beta.1",P.config=function(){return t.apply(this,k(arguments))},P.state=function(){return function(){return d(),{browser:x(function(t,e){for(var i={},n=0,o=e.length;n<o;n++)e[n]in t&&(i[e[n]]=t[e[n]]);return i}(g,["userAgent","platform","appName","appVersion"]),{isSupported:c()}),flash:function(t,e){var i,n={};for(i in t)-1===e.indexOf(i)&&(n[i]=t[i]);return n}(T,["bridge"]),zeroclipboard:{version:P.version,config:P.config()}}}.apply(this,k(arguments))},P.isFlashUnusable=function(){return function(){return!!(T.sandboxed||T.disabled||T.outdated||T.unavailable||T.degraded||T.deactivated)}.apply(this,k(arguments))},P.on=function(){return function(i,t){var e,n,o,s={};if("string"==typeof i&&i?o=i.toLowerCase().split(/\s+/):"object"!=typeof i||!i||"length"in i||void 0!==t||w(i).forEach(function(t){var e=i[t];"function"==typeof e&&P.on(t,e)}),o&&o.length&&t){for(e=0,n=o.length;e<n;e++)s[i=o[e].replace(/^on/,"")]=!0,S[i]||(S[i]=[]),S[i].push(t);if(s.ready&&T.ready&&P.emit({type:"ready"}),s.error){for(c()||P.emit({type:"error",name:"browser-unsupported"}),e=0,n=D.length;e<n;e++)if(!0===T[D[e].replace(/^flash-/,"")]){P.emit({type:"error",name:D[e]});break}u!==h&&P.version!==u&&P.emit({type:"error",name:"version-mismatch",jsVersion:P.version,swfVersion:u})}}return P}.apply(this,k(arguments))},P.off=function(){return function(i,t){var e,n,o,s,r;if(0===arguments.length?s=w(S):"string"==typeof i&&i?s=i.toLowerCase().split(/\s+/):"object"!=typeof i||!i||"length"in i||void 0!==t||w(i).forEach(function(t){var e=i[t];"function"==typeof e&&P.off(t,e)}),s&&s.length)for(e=0,n=s.length;e<n;e++)if(i=s[e].replace(/^on/,""),(r=S[i])&&r.length)if(t)for(o=r.indexOf(t);-1!==o;)r.splice(o,1),o=r.indexOf(t,o);else r.length=0;return P}.apply(this,k(arguments))},P.handlers=function(){return function(t){t="string"==typeof t&&t?s(S[t])||null:s(S);return t}.apply(this,k(arguments))},P.emit=function(){return i.apply(this,k(arguments))},P.create=function(){return H.apply(this,k(arguments))},P.destroy=function(){return F.apply(this,k(arguments))},P.setData=function(){return R.apply(this,k(arguments))},P.clearData=function(){return q.apply(this,k(arguments))},P.getData=function(){return function(t){return void 0===t?s(C):"string"==typeof t&&_.call(C,t)?C[t]:void 0}.apply(this,k(arguments))},P.focus=P.activate=function(){return Y.apply(this,k(arguments))},P.blur=P.deactivate=function(){return function(){var t=O(T.bridge);t&&(t.removeAttribute("title"),t.style.left="0px",t.style.top="-9999px",t.style.width="1px",t.style.height="1px"),l&&($(l,A.hoverClass),$(l,A.activeClass),l=null)}.apply(this,k(arguments))},P.activeElement=function(){return function(){return l||null}.apply(this,k(arguments))},0),N={},Pt=0,I={},Nt={};x(A,{autoActivate:!0});P._createClient=function(){!function(t){var e,i=this;i.id=""+jt++,N[i.id]=e={instance:i,elements:[],handlers:{},coreWildcardHandler:function(t){return i.emit(t)}},t&&i.clip(t),P.on("*",e.coreWildcardHandler),P.on("destroy",function(){i.destroy()}),P.create()}.apply(this,k(arguments))},P.prototype.on=function(){return function(i,t){var e,n,o,s={},r=this,a=N[r.id],l=a&&a.handlers;if(!a)throw new Error("Attempted to add new listener(s) to a destroyed ZeroClipboard client instance");if("string"==typeof i&&i?o=i.toLowerCase().split(/\s+/):"object"!=typeof i||!i||"length"in i||void 0!==t||w(i).forEach(function(t){var e=i[t];"function"==typeof e&&r.on(t,e)}),o&&o.length&&t){for(e=0,n=o.length;e<n;e++)s[i=o[e].replace(/^on/,"")]=!0,l[i]||(l[i]=[]),l[i].push(t);if(s.ready&&T.ready&&this.emit({type:"ready",client:this}),s.error){for(e=0,n=D.length;e<n;e++)if(T[D[e].replace(/^flash-/,"")]){this.emit({type:"error",name:D[e],client:this});break}u!==h&&P.version!==u&&this.emit({type:"error",name:"version-mismatch",jsVersion:P.version,swfVersion:u})}}return r}.apply(this,k(arguments))},P.prototype.off=function(){return function(i,t){var e,n,o,s,r,a=this,l=N[a.id],c=l&&l.handlers;if(c&&(0===arguments.length?s=w(c):"string"==typeof i&&i?s=i.split(/\s+/):"object"!=typeof i||!i||"length"in i||void 0!==t||w(i).forEach(function(t){var e=i[t];"function"==typeof e&&a.off(t,e)}),s)&&s.length)for(e=0,n=s.length;e<n;e++)if((r=c[i=s[e].toLowerCase().replace(/^on/,"")])&&r.length)if(t)for(o=r.indexOf(t);-1!==o;)r.splice(o,1),o=r.indexOf(t,o);else r.length=0;return a}.apply(this,k(arguments))},P.prototype.handlers=function(){return function(t){var e=null,i=N[this.id]&&N[this.id].handlers;return e=i?"string"==typeof t&&t?i[t]?i[t].slice(0):[]:s(i):e}.apply(this,k(arguments))},P.prototype.emit=function(){return z.apply(this,k(arguments))},P.prototype.clip=function(){return U.apply(this,k(arguments))},P.prototype.unclip=function(){return G.apply(this,k(arguments))},P.prototype.elements=function(){return function(){var t=N[this.id];return t&&t.elements?t.elements.slice(0):[]}.apply(this,k(arguments))},P.prototype.destroy=function(){return function(){var t=N[this.id];t&&(this.unclip(),this.off(),P.off("*",t.coreWildcardHandler),delete N[this.id])}.apply(this,k(arguments))},P.prototype.setText=function(t){if(N[this.id])return P.setData("text/plain",t),this;throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance")},P.prototype.setHtml=function(t){if(N[this.id])return P.setData("text/html",t),this;throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance")},P.prototype.setRichText=function(t){if(N[this.id])return P.setData("application/rtf",t),this;throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance")},P.prototype.setData=function(){if(N[this.id])return P.setData.apply(this,k(arguments)),this;throw new Error("Attempted to set pending clipboard data from a destroyed ZeroClipboard client instance")},P.prototype.clearData=function(){if(N[this.id])return P.clearData.apply(this,k(arguments)),this;throw new Error("Attempted to clear pending clipboard data from a destroyed ZeroClipboard client instance")},P.prototype.getData=function(){if(N[this.id])return P.getData.apply(this,k(arguments));throw new Error("Attempted to get pending clipboard data from a destroyed ZeroClipboard client instance")},"function"==typeof define&&define.amd?define(function(){return P}):"object"==typeof module&&module&&"object"==typeof module.exports&&module.exports?module.exports=P:r.ZeroClipboard=P}(function(){return this||window}()),!function(t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).Clipboard=t()}(function(){return function n(o,s,r){function a(i,t){if(!s[i]){if(!o[i]){var e="function"==typeof require&&require;if(!t&&e)return e(i,!0);if(l)return l(i,!0);t=new Error("Cannot find module '"+i+"'");throw t.code="MODULE_NOT_FOUND",t}e=s[i]={exports:{}};o[i][0].call(e.exports,function(t){var e=o[i][1][t];return a(e||t)},e,e.exports,n,o,s,r)}return s[i].exports}for(var l="function"==typeof require&&require,t=0;t<r.length;t++)a(r[t]);return a}({1:[function(t,e,i){var n;"undefined"==typeof Element||Element.prototype.matches||((n=Element.prototype).matches=n.matchesSelector||n.mozMatchesSelector||n.msMatchesSelector||n.oMatchesSelector||n.webkitMatchesSelector),e.exports=function(t,e){for(;t&&9!==t.nodeType;){if(t.matches(e))return t;t=t.parentNode}}},{}],2:[function(t,e,i){var r=t("./closest");e.exports=function(t,e,i,n,o){var s=function(e,i,t,n){return function(t){t.delegateTarget=r(t.target,i),t.delegateTarget&&n.call(e,t)}}.apply(this,arguments);return t.addEventListener(i,s,o),{destroy:function(){t.removeEventListener(i,s,o)}}}},{"./closest":1}],3:[function(t,e,i){i.node=function(t){return void 0!==t&&t instanceof HTMLElement&&1===t.nodeType},i.nodeList=function(t){var e=Object.prototype.toString.call(t);return void 0!==t&&("[object NodeList]"===e||"[object HTMLCollection]"===e)&&"length"in t&&(0===t.length||i.node(t[0]))},i.string=function(t){return"string"==typeof t||t instanceof String},i.fn=function(t){return"[object Function]"===Object.prototype.toString.call(t)}},{}],4:[function(t,e,i){var c=t("./is"),d=t("delegate");e.exports=function(t,e,i){if(!t&&!e&&!i)throw new Error("Missing required arguments");if(!c.string(e))throw new TypeError("Second argument must be a String");if(!c.fn(i))throw new TypeError("Third argument must be a Function");if(c.node(t))return a=e,l=i,(r=t).addEventListener(a,l),{destroy:function(){r.removeEventListener(a,l)}};if(c.nodeList(t))return n=t,o=e,s=i,Array.prototype.forEach.call(n,function(t){t.addEventListener(o,s)}),{destroy:function(){Array.prototype.forEach.call(n,function(t){t.removeEventListener(o,s)})}};if(c.string(t))return d(document.body,t,e,i);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList");var n,o,s,r,a,l}},{"./is":3,delegate:2}],5:[function(t,e,i){e.exports=function(t){var e,i;return t="SELECT"===t.nodeName?(t.focus(),t.value):"INPUT"===t.nodeName||"TEXTAREA"===t.nodeName?((e=t.hasAttribute("readonly"))||t.setAttribute("readonly",""),t.select(),t.setSelectionRange(0,t.value.length),e||t.removeAttribute("readonly"),t.value):(t.hasAttribute("contenteditable")&&t.focus(),e=window.getSelection(),(i=document.createRange()).selectNodeContents(t),e.removeAllRanges(),e.addRange(i),e.toString())}},{}],6:[function(t,e,i){function n(){}n.prototype={on:function(t,e,i){var n=this.e||(this.e={});return(n[t]||(n[t]=[])).push({fn:e,ctx:i}),this},once:function(t,e,i){var n=this;function o(){n.off(t,o),e.apply(i,arguments)}return o._=e,this.on(t,o,i)},emit:function(t){for(var e=[].slice.call(arguments,1),i=((this.e||(this.e={}))[t]||[]).slice(),n=0,o=i.length;n<o;n++)i[n].fn.apply(i[n].ctx,e);return this},off:function(t,e){var i=this.e||(this.e={}),n=i[t],o=[];if(n&&e)for(var s=0,r=n.length;s<r;s++)n[s].fn!==e&&n[s].fn._!==e&&o.push(n[s]);return o.length?i[t]=o:delete i[t],this}},e.exports=n},{}],7:[function(t,e,i){var n,o;n=this,o=function(t,e){var i=(e=e)&&e.__esModule?e:{default:e};var n="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t};function o(t,e){for(var i=0;i<e.length;i++){var n=e[i];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}(function(t,e,i){e&&o(t.prototype,e),i&&o(t,i)})(s,[{key:"resolveOptions",value:function(){var t=0<arguments.length&&void 0!==arguments[0]?arguments[0]:{};this.action=t.action,this.emitter=t.emitter,this.target=t.target,this.text=t.text,this.trigger=t.trigger,this.selectedText=""}},{key:"initSelection",value:function(){this.text?this.selectFake():this.target&&this.selectTarget()}},{key:"selectFake",value:function(){var t=this,e="rtl"==document.documentElement.getAttribute("dir"),e=(this.removeFake(),this.fakeHandlerCallback=function(){return t.removeFake()},this.fakeHandler=document.body.addEventListener("click",this.fakeHandlerCallback)||!0,this.fakeElem=document.createElement("textarea"),this.fakeElem.style.fontSize="12pt",this.fakeElem.style.border="0",this.fakeElem.style.padding="0",this.fakeElem.style.margin="0",this.fakeElem.style.position="absolute",this.fakeElem.style[e?"right":"left"]="-9999px",window.pageYOffset||document.documentElement.scrollTop);this.fakeElem.style.top=e+"px",this.fakeElem.setAttribute("readonly",""),this.fakeElem.value=this.text,document.body.appendChild(this.fakeElem),this.selectedText=(0,i.default)(this.fakeElem),this.copyText()}},{key:"removeFake",value:function(){this.fakeHandler&&(document.body.removeEventListener("click",this.fakeHandlerCallback),this.fakeHandler=null,this.fakeHandlerCallback=null),this.fakeElem&&(document.body.removeChild(this.fakeElem),this.fakeElem=null)}},{key:"selectTarget",value:function(){this.selectedText=(0,i.default)(this.target),this.copyText()}},{key:"copyText",value:function(){var e=void 0;try{e=document.execCommand(this.action)}catch(t){e=!1}this.handleResult(e)}},{key:"handleResult",value:function(t){this.emitter.emit(t?"success":"error",{action:this.action,text:this.selectedText,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)})}},{key:"clearSelection",value:function(){this.target&&this.target.blur(),window.getSelection().removeAllRanges()}},{key:"destroy",value:function(){this.removeFake()}},{key:"action",set:function(){if(this._action=0<arguments.length&&void 0!==arguments[0]?arguments[0]:"copy","copy"!==this._action&&"cut"!==this._action)throw new Error('Invalid "action" value, use either "copy" or "cut"')},get:function(){return this._action}},{key:"target",set:function(t){if(void 0!==t){if(!t||"object"!==(void 0===t?"undefined":n(t))||1!==t.nodeType)throw new Error('Invalid "target" value, use a valid Element');if("copy"===this.action&&t.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if("cut"===this.action&&(t.hasAttribute("readonly")||t.hasAttribute("disabled")))throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');this._target=t}},get:function(){return this._target}}]);e=s;function s(t){if(!(this instanceof s))throw new TypeError("Cannot call a class as a function");this.resolveOptions(t),this.initSelection()}t.exports=e},void 0!==i?o(e,t("select")):(o(o={exports:{}},n.select),n.clipboardAction=o.exports)},{select:5}],8:[function(t,e,i){var n,o;n=this,o=function(t,e,i,n){var o=r(e),e=r(i),s=r(n);function r(t){return t&&t.__esModule?t:{default:t}}var a=function(t,e,i){return e&&l(t.prototype,e),i&&l(t,i),t};function l(t,e){for(var i=0;i<e.length;i++){var n=e[i];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}i=function(t){var e=n;if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);function n(t,e){var i;if(this instanceof n)return(i=function(t,e){if(t)return!e||"object"!=typeof e&&"function"!=typeof e?t:e;throw new ReferenceError("this hasn't been initialised - super() hasn't been called")}(this,(n.__proto__||Object.getPrototypeOf(n)).call(this))).resolveOptions(e),i.listenClick(t),i;throw new TypeError("Cannot call a class as a function")}return e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t),a(n,[{key:"resolveOptions",value:function(){var t=0<arguments.length&&void 0!==arguments[0]?arguments[0]:{};this.action="function"==typeof t.action?t.action:this.defaultAction,this.target="function"==typeof t.target?t.target:this.defaultTarget,this.text="function"==typeof t.text?t.text:this.defaultText}},{key:"listenClick",value:function(t){var e=this;this.listener=(0,s.default)(t,"click",function(t){return e.onClick(t)})}},{key:"onClick",value:function(t){t=t.delegateTarget||t.currentTarget;this.clipboardAction&&(this.clipboardAction=null),this.clipboardAction=new o.default({action:this.action(t),target:this.target(t),text:this.text(t),trigger:t,emitter:this})}},{key:"defaultAction",value:function(t){return c("action",t)}},{key:"defaultTarget",value:function(t){t=c("target",t);if(t)return document.querySelector(t)}},{key:"defaultText",value:function(t){return c("text",t)}},{key:"destroy",value:function(){this.listener.destroy(),this.clipboardAction&&(this.clipboardAction.destroy(),this.clipboardAction=null)}}],[{key:"isSupported",value:function(){var t=0<arguments.length&&void 0!==arguments[0]?arguments[0]:["copy","cut"],t="string"==typeof t?[t]:t,e=!!document.queryCommandSupported;return t.forEach(function(t){e=e&&!!document.queryCommandSupported(t)}),e}}]),n}(e.default);function c(t,e){t="data-clipboard-"+t;if(e.hasAttribute(t))return e.getAttribute(t)}t.exports=i},void 0!==i?o(e,t("./clipboard-action"),t("tiny-emitter"),t("good-listener")):(o(o={exports:{}},n.clipboardAction,n.tinyEmitter,n.goodListener),n.clipboard=o.exports)},{"./clipboard-action":7,"good-listener":4,"tiny-emitter":6}]},{},[8])(8)}),!function(t){"use strict";"function"==typeof define&&define.amd?define(["jquery"],t):"undefined"!=typeof exports?module.exports=t(require("jquery")):t(jQuery)}(function(c){"use strict";var n,s=window.Slick||{};n=0,(s=function(t,e){var i=this;i.defaults={accessibility:!0,adaptiveHeight:!1,appendArrows:c(t),appendDots:c(t),arrows:!0,asNavFor:null,prevArrow:'<button class="slick-prev" aria-label="Previous" type="button">Previous</button>',nextArrow:'<button class="slick-next" aria-label="Next" type="button">Next</button>',autoplay:!1,autoplaySpeed:3e3,centerMode:!1,centerPadding:"50px",cssEase:"ease",customPaging:function(t,e){return c('<button type="button" />').text(e+1)},dots:!1,dotsClass:"slick-dots",draggable:!0,easing:"linear",edgeFriction:.35,fade:!1,focusOnSelect:!1,focusOnChange:!1,infinite:!0,initialSlide:0,lazyLoad:"ondemand",mobileFirst:!1,pauseOnHover:!0,pauseOnFocus:!0,pauseOnDotsHover:!1,respondTo:"window",responsive:null,rows:1,rtl:!1,slide:"",slidesPerRow:1,slidesToShow:1,slidesToScroll:1,speed:500,swipe:!0,swipeToSlide:!1,touchMove:!0,touchThreshold:5,useCSS:!0,useTransform:!0,variableWidth:!1,vertical:!1,verticalSwiping:!1,waitForAnimate:!0,zIndex:1e3},i.initials={animating:!1,dragging:!1,autoPlayTimer:null,currentDirection:0,currentLeft:null,currentSlide:0,direction:1,$dots:null,listWidth:null,listHeight:null,loadIndex:0,$nextArrow:null,$prevArrow:null,scrolling:!1,slideCount:null,slideWidth:null,$slideTrack:null,$slides:null,sliding:!1,slideOffset:0,swipeLeft:null,swiping:!1,$list:null,touchObject:{},transformsEnabled:!1,unslicked:!1},c.extend(i,i.initials),i.activeBreakpoint=null,i.animType=null,i.animProp=null,i.breakpoints=[],i.breakpointSettings=[],i.cssTransitions=!1,i.focussed=!1,i.interrupted=!1,i.hidden="hidden",i.paused=!0,i.positionProp=null,i.respondTo=null,i.rowCount=1,i.shouldClick=!0,i.$slider=c(t),i.$slidesCache=null,i.transformType=null,i.transitionType=null,i.visibilityChange="visibilitychange",i.windowWidth=0,i.windowTimer=null,t=c(t).data("slick")||{},i.options=c.extend({},i.defaults,e,t),i.currentSlide=i.options.initialSlide,i.originalSettings=i.options,void 0!==document.mozHidden?(i.hidden="mozHidden",i.visibilityChange="mozvisibilitychange"):void 0!==document.webkitHidden&&(i.hidden="webkitHidden",i.visibilityChange="webkitvisibilitychange"),i.autoPlay=c.proxy(i.autoPlay,i),i.autoPlayClear=c.proxy(i.autoPlayClear,i),i.autoPlayIterator=c.proxy(i.autoPlayIterator,i),i.changeSlide=c.proxy(i.changeSlide,i),i.clickHandler=c.proxy(i.clickHandler,i),i.selectHandler=c.proxy(i.selectHandler,i),i.setPosition=c.proxy(i.setPosition,i),i.swipeHandler=c.proxy(i.swipeHandler,i),i.dragHandler=c.proxy(i.dragHandler,i),i.keyHandler=c.proxy(i.keyHandler,i),i.instanceUid=n++,i.htmlExpr=/^(?:\s*(<[\w\W]+>)[^>]*)$/,i.registerBreakpoints(),i.init(!0)}).prototype.activateADA=function(){this.$slideTrack.find(".slick-active").attr({"aria-hidden":"false"}).find("a, input, button, select").attr({tabindex:"0"})},s.prototype.addSlide=s.prototype.slickAdd=function(t,e,i){var n=this;if("boolean"==typeof e)i=e,e=null;else if(e<0||e>=n.slideCount)return!1;n.unload(),"number"==typeof e?0===e&&0===n.$slides.length?c(t).appendTo(n.$slideTrack):i?c(t).insertBefore(n.$slides.eq(e)):c(t).insertAfter(n.$slides.eq(e)):!0===i?c(t).prependTo(n.$slideTrack):c(t).appendTo(n.$slideTrack),n.$slides=n.$slideTrack.children(this.options.slide),n.$slideTrack.children(this.options.slide).detach(),n.$slideTrack.append(n.$slides),n.$slides.each(function(t,e){c(e).attr("data-slick-index",t)}),n.$slidesCache=n.$slides,n.reinit()},s.prototype.animateHeight=function(){var t;1===this.options.slidesToShow&&!0===this.options.adaptiveHeight&&!1===this.options.vertical&&(t=this.$slides.eq(this.currentSlide).outerHeight(!0),this.$list.animate({height:t},this.options.speed))},s.prototype.animateSlide=function(t,e){var i={},n=this;n.animateHeight(),!0===n.options.rtl&&!1===n.options.vertical&&(t=-t),!1===n.transformsEnabled?!1===n.options.vertical?n.$slideTrack.animate({left:t},n.options.speed,n.options.easing,e):n.$slideTrack.animate({top:t},n.options.speed,n.options.easing,e):!1===n.cssTransitions?(!0===n.options.rtl&&(n.currentLeft=-n.currentLeft),c({animStart:n.currentLeft}).animate({animStart:t},{duration:n.options.speed,easing:n.options.easing,step:function(t){t=Math.ceil(t),!1===n.options.vertical?i[n.animType]="translate("+t+"px, 0px)":i[n.animType]="translate(0px,"+t+"px)",n.$slideTrack.css(i)},complete:function(){e&&e.call()}})):(n.applyTransition(),t=Math.ceil(t),!1===n.options.vertical?i[n.animType]="translate3d("+t+"px, 0px, 0px)":i[n.animType]="translate3d(0px,"+t+"px, 0px)",n.$slideTrack.css(i),e&&setTimeout(function(){n.disableTransition(),e.call()},n.options.speed))},s.prototype.getNavTarget=function(){var t=this.options.asNavFor;return t=t&&null!==t?c(t).not(this.$slider):t},s.prototype.asNavFor=function(e){var t=this.getNavTarget();null!==t&&"object"==typeof t&&t.each(function(){var t=c(this).slick("getSlick");t.unslicked||t.slideHandler(e,!0)})},s.prototype.applyTransition=function(t){var e=this,i={};!1===e.options.fade?i[e.transitionType]=e.transformType+" "+e.options.speed+"ms "+e.options.cssEase:i[e.transitionType]="opacity "+e.options.speed+"ms "+e.options.cssEase,(!1===e.options.fade?e.$slideTrack:e.$slides.eq(t)).css(i)},s.prototype.autoPlay=function(){this.autoPlayClear(),this.slideCount>this.options.slidesToShow&&(this.autoPlayTimer=setInterval(this.autoPlayIterator,this.options.autoplaySpeed))},s.prototype.autoPlayClear=function(){this.autoPlayTimer&&clearInterval(this.autoPlayTimer)},s.prototype.autoPlayIterator=function(){var t=this,e=t.currentSlide+t.options.slidesToScroll;t.paused||t.interrupted||t.focussed||(!1===t.options.infinite&&(1===t.direction&&t.currentSlide+1===t.slideCount-1?t.direction=0:0===t.direction&&(e=t.currentSlide-t.options.slidesToScroll,t.currentSlide-1==0)&&(t.direction=1)),t.slideHandler(e))},s.prototype.buildArrows=function(){var t=this;!0===t.options.arrows&&(t.$prevArrow=c(t.options.prevArrow).addClass("slick-arrow"),t.$nextArrow=c(t.options.nextArrow).addClass("slick-arrow"),t.slideCount>t.options.slidesToShow?(t.$prevArrow.removeClass("slick-hidden").removeAttr("aria-hidden tabindex"),t.$nextArrow.removeClass("slick-hidden").removeAttr("aria-hidden tabindex"),t.htmlExpr.test(t.options.prevArrow)&&t.$prevArrow.prependTo(t.options.appendArrows),t.htmlExpr.test(t.options.nextArrow)&&t.$nextArrow.appendTo(t.options.appendArrows),!0!==t.options.infinite&&t.$prevArrow.addClass("slick-disabled").attr("aria-disabled","true")):t.$prevArrow.add(t.$nextArrow).addClass("slick-hidden").attr({"aria-disabled":"true",tabindex:"-1"}))},s.prototype.buildDots=function(){var t,e;if(!0===this.options.dots){for(this.$slider.addClass("slick-dotted"),e=c("<ul />").addClass(this.options.dotsClass),t=0;t<=this.getDotCount();t+=1)e.append(c("<li />").append(this.options.customPaging.call(this,this,t)));this.$dots=e.appendTo(this.options.appendDots),this.$dots.find("li").first().addClass("slick-active")}},s.prototype.buildOut=function(){var t=this;t.$slides=t.$slider.children(t.options.slide+":not(.slick-cloned)").addClass("slick-slide"),t.slideCount=t.$slides.length,t.$slides.each(function(t,e){c(e).attr("data-slick-index",t).data("originalStyling",c(e).attr("style")||"")}),t.$slider.addClass("slick-slider"),t.$slideTrack=0===t.slideCount?c('<div class="slick-track"/>').appendTo(t.$slider):t.$slides.wrapAll('<div class="slick-track"/>').parent(),t.$list=t.$slideTrack.wrap('<div class="slick-list"/>').parent(),t.$slideTrack.css("opacity",0),!0!==t.options.centerMode&&!0!==t.options.swipeToSlide||(t.options.slidesToScroll=1),c("img[data-lazy]",t.$slider).not("[src]").addClass("slick-loading"),t.setupInfinite(),t.buildArrows(),t.buildDots(),t.updateDots(),t.setSlideClasses("number"==typeof t.currentSlide?t.currentSlide:0),!0===t.options.draggable&&t.$list.addClass("draggable")},s.prototype.buildRows=function(){var t,e,i,n=this,o=document.createDocumentFragment(),s=n.$slider.children();if(1<n.options.rows){for(i=n.options.slidesPerRow*n.options.rows,e=Math.ceil(s.length/i),t=0;t<e;t++){for(var r=document.createElement("div"),a=0;a<n.options.rows;a++){for(var l=document.createElement("div"),c=0;c<n.options.slidesPerRow;c++){var d=t*i+(a*n.options.slidesPerRow+c);s.get(d)&&l.appendChild(s.get(d))}r.appendChild(l)}o.appendChild(r)}n.$slider.empty().append(o),n.$slider.children().children().children().css({width:100/n.options.slidesPerRow+"%",display:"inline-block"})}},s.prototype.checkResponsive=function(t,e){var i,n,o,s=this,r=!1,a=s.$slider.width(),l=window.innerWidth||c(window).width();if("window"===s.respondTo?o=l:"slider"===s.respondTo?o=a:"min"===s.respondTo&&(o=Math.min(l,a)),s.options.responsive&&s.options.responsive.length&&null!==s.options.responsive){for(i in n=null,s.breakpoints)s.breakpoints.hasOwnProperty(i)&&(!1===s.originalSettings.mobileFirst?o<s.breakpoints[i]&&(n=s.breakpoints[i]):o>s.breakpoints[i]&&(n=s.breakpoints[i]));null!==n?null!==s.activeBreakpoint&&n===s.activeBreakpoint&&!e||(s.activeBreakpoint=n,"unslick"===s.breakpointSettings[n]?s.unslick(n):(s.options=c.extend({},s.originalSettings,s.breakpointSettings[n]),!0===t&&(s.currentSlide=s.options.initialSlide),s.refresh(t)),r=n):null!==s.activeBreakpoint&&(s.activeBreakpoint=null,s.options=s.originalSettings,!0===t&&(s.currentSlide=s.options.initialSlide),s.refresh(t),r=n),t||!1===r||s.$slider.trigger("breakpoint",[s,r])}},s.prototype.changeSlide=function(t,e){var i,n=this,o=c(t.currentTarget);switch(o.is("a")&&t.preventDefault(),o.is("li")||(o=o.closest("li")),i=n.slideCount%n.options.slidesToScroll!=0?0:(n.slideCount-n.currentSlide)%n.options.slidesToScroll,t.data.message){case"previous":s=0==i?n.options.slidesToScroll:n.options.slidesToShow-i,n.slideCount>n.options.slidesToShow&&n.slideHandler(n.currentSlide-s,!1,e);break;case"next":s=0==i?n.options.slidesToScroll:i,n.slideCount>n.options.slidesToShow&&n.slideHandler(n.currentSlide+s,!1,e);break;case"index":var s=0===t.data.index?0:t.data.index||o.index()*n.options.slidesToScroll;n.slideHandler(n.checkNavigable(s),!1,e),o.children().trigger("focus");break;default:return}},s.prototype.checkNavigable=function(t){var e=this.getNavigableIndexes(),i=0;if(t>e[e.length-1])t=e[e.length-1];else for(var n in e){if(t<e[n]){t=i;break}i=e[n]}return t},s.prototype.cleanUpEvents=function(){var t=this;t.options.dots&&null!==t.$dots&&(c("li",t.$dots).off("click.slick",t.changeSlide).off("mouseenter.slick",c.proxy(t.interrupt,t,!0)).off("mouseleave.slick",c.proxy(t.interrupt,t,!1)),!0===t.options.accessibility)&&t.$dots.off("keydown.slick",t.keyHandler),t.$slider.off("focus.slick blur.slick"),!0===t.options.arrows&&t.slideCount>t.options.slidesToShow&&(t.$prevArrow&&t.$prevArrow.off("click.slick",t.changeSlide),t.$nextArrow&&t.$nextArrow.off("click.slick",t.changeSlide),!0===t.options.accessibility)&&(t.$prevArrow&&t.$prevArrow.off("keydown.slick",t.keyHandler),t.$nextArrow)&&t.$nextArrow.off("keydown.slick",t.keyHandler),t.$list.off("touchstart.slick mousedown.slick",t.swipeHandler),t.$list.off("touchmove.slick mousemove.slick",t.swipeHandler),t.$list.off("touchend.slick mouseup.slick",t.swipeHandler),t.$list.off("touchcancel.slick mouseleave.slick",t.swipeHandler),t.$list.off("click.slick",t.clickHandler),c(document).off(t.visibilityChange,t.visibility),t.cleanUpSlideEvents(),!0===t.options.accessibility&&t.$list.off("keydown.slick",t.keyHandler),!0===t.options.focusOnSelect&&c(t.$slideTrack).children().off("click.slick",t.selectHandler),c(window).off("orientationchange.slick.slick-"+t.instanceUid,t.orientationChange),c(window).off("resize.slick.slick-"+t.instanceUid,t.resize),c("[draggable!=true]",t.$slideTrack).off("dragstart",t.preventDefault),c(window).off("load.slick.slick-"+t.instanceUid,t.setPosition)},s.prototype.cleanUpSlideEvents=function(){this.$list.off("mouseenter.slick",c.proxy(this.interrupt,this,!0)),this.$list.off("mouseleave.slick",c.proxy(this.interrupt,this,!1))},s.prototype.cleanUpRows=function(){var t;1<this.options.rows&&((t=this.$slides.children().children()).removeAttr("style"),this.$slider.empty().append(t))},s.prototype.clickHandler=function(t){!1===this.shouldClick&&(t.stopImmediatePropagation(),t.stopPropagation(),t.preventDefault())},s.prototype.destroy=function(t){var e=this;e.autoPlayClear(),e.touchObject={},e.cleanUpEvents(),c(".slick-cloned",e.$slider).detach(),e.$dots&&e.$dots.remove(),e.$prevArrow&&e.$prevArrow.length&&(e.$prevArrow.removeClass("slick-disabled slick-arrow slick-hidden").removeAttr("aria-hidden aria-disabled tabindex").css("display",""),e.htmlExpr.test(e.options.prevArrow))&&e.$prevArrow.remove(),e.$nextArrow&&e.$nextArrow.length&&(e.$nextArrow.removeClass("slick-disabled slick-arrow slick-hidden").removeAttr("aria-hidden aria-disabled tabindex").css("display",""),e.htmlExpr.test(e.options.nextArrow))&&e.$nextArrow.remove(),e.$slides&&(e.$slides.removeClass("slick-slide slick-active slick-center slick-visible slick-current").removeAttr("aria-hidden").removeAttr("data-slick-index").each(function(){c(this).attr("style",c(this).data("originalStyling"))}),e.$slideTrack.children(this.options.slide).detach(),e.$slideTrack.detach(),e.$list.detach(),e.$slider.append(e.$slides)),e.cleanUpRows(),e.$slider.removeClass("slick-slider"),e.$slider.removeClass("slick-initialized"),e.$slider.removeClass("slick-dotted"),e.unslicked=!0,t||e.$slider.trigger("destroy",[e])},s.prototype.disableTransition=function(t){var e={};e[this.transitionType]="",(!1===this.options.fade?this.$slideTrack:this.$slides.eq(t)).css(e)},s.prototype.fadeSlide=function(t,e){var i=this;!1===i.cssTransitions?(i.$slides.eq(t).css({zIndex:i.options.zIndex}),i.$slides.eq(t).animate({opacity:1},i.options.speed,i.options.easing,e)):(i.applyTransition(t),i.$slides.eq(t).css({opacity:1,zIndex:i.options.zIndex}),e&&setTimeout(function(){i.disableTransition(t),e.call()},i.options.speed))},s.prototype.fadeSlideOut=function(t){!1===this.cssTransitions?this.$slides.eq(t).animate({opacity:0,zIndex:this.options.zIndex-2},this.options.speed,this.options.easing):(this.applyTransition(t),this.$slides.eq(t).css({opacity:0,zIndex:this.options.zIndex-2}))},s.prototype.filterSlides=s.prototype.slickFilter=function(t){null!==t&&(this.$slidesCache=this.$slides,this.unload(),this.$slideTrack.children(this.options.slide).detach(),this.$slidesCache.filter(t).appendTo(this.$slideTrack),this.reinit())},s.prototype.focusHandler=function(){var i=this;i.$slider.off("focus.slick blur.slick").on("focus.slick blur.slick","*",function(t){t.stopImmediatePropagation();var e=c(this);setTimeout(function(){i.options.pauseOnFocus&&(i.focussed=e.is(":focus"),i.autoPlay())},0)})},s.prototype.getCurrent=s.prototype.slickCurrentSlide=function(){return this.currentSlide},s.prototype.getDotCount=function(){var t=this,e=0,i=0,n=0;if(!0===t.options.infinite)if(t.slideCount<=t.options.slidesToShow)++n;else for(;e<t.slideCount;)++n,e=i+t.options.slidesToScroll,i+=t.options.slidesToScroll<=t.options.slidesToShow?t.options.slidesToScroll:t.options.slidesToShow;else if(!0===t.options.centerMode)n=t.slideCount;else if(t.options.asNavFor)for(;e<t.slideCount;)++n,e=i+t.options.slidesToScroll,i+=t.options.slidesToScroll<=t.options.slidesToShow?t.options.slidesToScroll:t.options.slidesToShow;else n=1+Math.ceil((t.slideCount-t.options.slidesToShow)/t.options.slidesToScroll);return n-1},s.prototype.getLeft=function(t){var e,i,n=this,o=0;return n.slideOffset=0,e=n.$slides.first().outerHeight(!0),!0===n.options.infinite?(n.slideCount>n.options.slidesToShow&&(n.slideOffset=n.slideWidth*n.options.slidesToShow*-1,i=-1,!0===n.options.vertical&&!0===n.options.centerMode&&(2===n.options.slidesToShow?i=-1.5:1===n.options.slidesToShow&&(i=-2)),o=e*n.options.slidesToShow*i),n.slideCount%n.options.slidesToScroll!=0&&t+n.options.slidesToScroll>n.slideCount&&n.slideCount>n.options.slidesToShow&&(o=t>n.slideCount?(n.slideOffset=(n.options.slidesToShow-(t-n.slideCount))*n.slideWidth*-1,(n.options.slidesToShow-(t-n.slideCount))*e*-1):(n.slideOffset=n.slideCount%n.options.slidesToScroll*n.slideWidth*-1,n.slideCount%n.options.slidesToScroll*e*-1))):t+n.options.slidesToShow>n.slideCount&&(n.slideOffset=(t+n.options.slidesToShow-n.slideCount)*n.slideWidth,o=(t+n.options.slidesToShow-n.slideCount)*e),n.slideCount<=n.options.slidesToShow&&(o=n.slideOffset=0),!0===n.options.centerMode&&n.slideCount<=n.options.slidesToShow?n.slideOffset=n.slideWidth*Math.floor(n.options.slidesToShow)/2-n.slideWidth*n.slideCount/2:!0===n.options.centerMode&&!0===n.options.infinite?n.slideOffset+=n.slideWidth*Math.floor(n.options.slidesToShow/2)-n.slideWidth:!0===n.options.centerMode&&(n.slideOffset=0,n.slideOffset+=n.slideWidth*Math.floor(n.options.slidesToShow/2)),i=!1===n.options.vertical?t*n.slideWidth*-1+n.slideOffset:t*e*-1+o,!0===n.options.variableWidth&&(e=n.slideCount<=n.options.slidesToShow||!1===n.options.infinite?n.$slideTrack.children(".slick-slide").eq(t):n.$slideTrack.children(".slick-slide").eq(t+n.options.slidesToShow),i=!0===n.options.rtl?e[0]?-1*(n.$slideTrack.width()-e[0].offsetLeft-e.width()):0:e[0]?-1*e[0].offsetLeft:0,!0===n.options.centerMode)&&(e=n.slideCount<=n.options.slidesToShow||!1===n.options.infinite?n.$slideTrack.children(".slick-slide").eq(t):n.$slideTrack.children(".slick-slide").eq(t+n.options.slidesToShow+1),i=!0===n.options.rtl?e[0]?-1*(n.$slideTrack.width()-e[0].offsetLeft-e.width()):0:e[0]?-1*e[0].offsetLeft:0,i+=(n.$list.width()-e.outerWidth())/2),i},s.prototype.getOption=s.prototype.slickGetOption=function(t){return this.options[t]},s.prototype.getNavigableIndexes=function(){for(var t=this,e=0,i=0,n=[],o=!1===t.options.infinite?t.slideCount:(e=-1*t.options.slidesToScroll,i=-1*t.options.slidesToScroll,2*t.slideCount);e<o;)n.push(e),e=i+t.options.slidesToScroll,i+=t.options.slidesToScroll<=t.options.slidesToShow?t.options.slidesToScroll:t.options.slidesToShow;return n},s.prototype.getSlick=function(){return this},s.prototype.getSlideCount=function(){var i,n=this,o=!0===n.options.centerMode?n.slideWidth*Math.floor(n.options.slidesToShow/2):0;return!0===n.options.swipeToSlide?(n.$slideTrack.find(".slick-slide").each(function(t,e){if(e.offsetLeft-o+c(e).outerWidth()/2>-1*n.swipeLeft)return i=e,!1}),Math.abs(c(i).attr("data-slick-index")-n.currentSlide)||1):n.options.slidesToScroll},s.prototype.goTo=s.prototype.slickGoTo=function(t,e){this.changeSlide({data:{message:"index",index:parseInt(t)}},e)},s.prototype.init=function(t){var e=this;c(e.$slider).hasClass("slick-initialized")||(c(e.$slider).addClass("slick-initialized"),e.buildRows(),e.buildOut(),e.setProps(),e.startLoad(),e.loadSlider(),e.initializeEvents(),e.updateArrows(),e.updateDots(),e.checkResponsive(!0),e.focusHandler()),t&&e.$slider.trigger("init",[e]),!0===e.options.accessibility&&e.initADA(),e.options.autoplay&&(e.paused=!1,e.autoPlay())},s.prototype.initADA=function(){var i=this,n=Math.ceil(i.slideCount/i.options.slidesToShow),o=i.getNavigableIndexes().filter(function(t){return 0<=t&&t<i.slideCount});i.$slides.add(i.$slideTrack.find(".slick-cloned")).attr({"aria-hidden":"true",tabindex:"-1"}).find("a, input, button, select").attr({tabindex:"-1"}),null!==i.$dots&&(i.$slides.not(i.$slideTrack.find(".slick-cloned")).each(function(t){var e=o.indexOf(t);c(this).attr({role:"tabpanel",id:"slick-slide"+i.instanceUid+t,tabindex:-1}),-1!==e&&c(this).attr({"aria-describedby":"slick-slide-control"+i.instanceUid+e})}),i.$dots.attr("role","tablist").find("li").each(function(t){var e=o[t];c(this).attr({role:"presentation"}),c(this).find("button").first().attr({role:"tab",id:"slick-slide-control"+i.instanceUid+t,"aria-controls":"slick-slide"+i.instanceUid+e,"aria-label":t+1+" of "+n,"aria-selected":null,tabindex:"-1"})}).eq(i.currentSlide).find("button").attr({"aria-selected":"true",tabindex:"0"}).end());for(var t=i.currentSlide,e=t+i.options.slidesToShow;t<e;t++)i.$slides.eq(t).attr("tabindex",0);i.activateADA()},s.prototype.initArrowEvents=function(){var t=this;!0===t.options.arrows&&t.slideCount>t.options.slidesToShow&&(t.$prevArrow.off("click.slick").on("click.slick",{message:"previous"},t.changeSlide),t.$nextArrow.off("click.slick").on("click.slick",{message:"next"},t.changeSlide),!0===t.options.accessibility)&&(t.$prevArrow.on("keydown.slick",t.keyHandler),t.$nextArrow.on("keydown.slick",t.keyHandler))},s.prototype.initDotEvents=function(){var t=this;!0===t.options.dots&&(c("li",t.$dots).on("click.slick",{message:"index"},t.changeSlide),!0===t.options.accessibility)&&t.$dots.on("keydown.slick",t.keyHandler),!0===t.options.dots&&!0===t.options.pauseOnDotsHover&&c("li",t.$dots).on("mouseenter.slick",c.proxy(t.interrupt,t,!0)).on("mouseleave.slick",c.proxy(t.interrupt,t,!1))},s.prototype.initSlideEvents=function(){this.options.pauseOnHover&&(this.$list.on("mouseenter.slick",c.proxy(this.interrupt,this,!0)),this.$list.on("mouseleave.slick",c.proxy(this.interrupt,this,!1)))},s.prototype.initializeEvents=function(){var t=this;t.initArrowEvents(),t.initDotEvents(),t.initSlideEvents(),t.$list.on("touchstart.slick mousedown.slick",{action:"start"},t.swipeHandler),t.$list.on("touchmove.slick mousemove.slick",{action:"move"},t.swipeHandler),t.$list.on("touchend.slick mouseup.slick",{action:"end"},t.swipeHandler),t.$list.on("touchcancel.slick mouseleave.slick",{action:"end"},t.swipeHandler),t.$list.on("click.slick",t.clickHandler),c(document).on(t.visibilityChange,c.proxy(t.visibility,t)),!0===t.options.accessibility&&t.$list.on("keydown.slick",t.keyHandler),!0===t.options.focusOnSelect&&c(t.$slideTrack).children().on("click.slick",t.selectHandler),c(window).on("orientationchange.slick.slick-"+t.instanceUid,c.proxy(t.orientationChange,t)),c(window).on("resize.slick.slick-"+t.instanceUid,c.proxy(t.resize,t)),c("[draggable!=true]",t.$slideTrack).on("dragstart",t.preventDefault),c(window).on("load.slick.slick-"+t.instanceUid,t.setPosition),c(t.setPosition)},s.prototype.initUI=function(){!0===this.options.arrows&&this.slideCount>this.options.slidesToShow&&(this.$prevArrow.show(),this.$nextArrow.show()),!0===this.options.dots&&this.slideCount>this.options.slidesToShow&&this.$dots.show()},s.prototype.keyHandler=function(t){t.target.tagName.match("TEXTAREA|INPUT|SELECT")||(37===t.keyCode&&!0===this.options.accessibility?this.changeSlide({data:{message:!0===this.options.rtl?"next":"previous"}}):39===t.keyCode&&!0===this.options.accessibility&&this.changeSlide({data:{message:!0===this.options.rtl?"previous":"next"}}))},s.prototype.lazyLoad=function(){function t(t){c("img[data-lazy]",t).each(function(){var t=c(this),e=c(this).attr("data-lazy"),i=c(this).attr("data-srcset"),n=c(this).attr("data-sizes")||s.$slider.attr("data-sizes"),o=document.createElement("img");o.onload=function(){t.animate({opacity:0},100,function(){i&&(t.attr("srcset",i),n)&&t.attr("sizes",n),t.attr("src",e).animate({opacity:1},200,function(){t.removeAttr("data-lazy data-srcset data-sizes").removeClass("slick-loading")}),s.$slider.trigger("lazyLoaded",[s,t,e])})},o.onerror=function(){t.removeAttr("data-lazy").removeClass("slick-loading").addClass("slick-lazyload-error"),s.$slider.trigger("lazyLoadError",[s,t,e])},o.src=e})}var e,i,n,s=this;if(!0===s.options.centerMode?n=!0===s.options.infinite?(i=s.currentSlide+(s.options.slidesToShow/2+1))+s.options.slidesToShow+2:(i=Math.max(0,s.currentSlide-(s.options.slidesToShow/2+1)),s.options.slidesToShow/2+1+2+s.currentSlide):(i=s.options.infinite?s.options.slidesToShow+s.currentSlide:s.currentSlide,n=Math.ceil(i+s.options.slidesToShow),!0===s.options.fade&&(0<i&&i--,n<=s.slideCount)&&n++),e=s.$slider.find(".slick-slide").slice(i,n),"anticipated"===s.options.lazyLoad)for(var o=i-1,r=n,a=s.$slider.find(".slick-slide"),l=0;l<s.options.slidesToScroll;l++)o<0&&(o=s.slideCount-1),e=(e=e.add(a.eq(o))).add(a.eq(r)),o--,r++;t(e),s.slideCount<=s.options.slidesToShow?t(s.$slider.find(".slick-slide")):s.currentSlide>=s.slideCount-s.options.slidesToShow?t(s.$slider.find(".slick-cloned").slice(0,s.options.slidesToShow)):0===s.currentSlide&&t(s.$slider.find(".slick-cloned").slice(-1*s.options.slidesToShow))},s.prototype.loadSlider=function(){this.setPosition(),this.$slideTrack.css({opacity:1}),this.$slider.removeClass("slick-loading"),this.initUI(),"progressive"===this.options.lazyLoad&&this.progressiveLazyLoad()},s.prototype.next=s.prototype.slickNext=function(){this.changeSlide({data:{message:"next"}})},s.prototype.orientationChange=function(){this.checkResponsive(),this.setPosition()},s.prototype.pause=s.prototype.slickPause=function(){this.autoPlayClear(),this.paused=!0},s.prototype.play=s.prototype.slickPlay=function(){this.autoPlay(),this.options.autoplay=!0,this.paused=!1,this.focussed=!1,this.interrupted=!1},s.prototype.postSlide=function(t){var e=this;e.unslicked||(e.$slider.trigger("afterChange",[e,t]),e.animating=!1,e.slideCount>e.options.slidesToShow&&e.setPosition(),e.swipeLeft=null,e.options.autoplay&&e.autoPlay(),!0===e.options.accessibility&&(e.initADA(),e.options.focusOnChange)&&c(e.$slides.get(e.currentSlide)).attr("tabindex",0).focus())},s.prototype.prev=s.prototype.slickPrev=function(){this.changeSlide({data:{message:"previous"}})},s.prototype.preventDefault=function(t){t.preventDefault()},s.prototype.progressiveLazyLoad=function(t){t=t||1;var e,i,n,o,s=this,r=c("img[data-lazy]",s.$slider);r.length?(e=r.first(),i=e.attr("data-lazy"),n=e.attr("data-srcset"),o=e.attr("data-sizes")||s.$slider.attr("data-sizes"),(r=document.createElement("img")).onload=function(){n&&(e.attr("srcset",n),o)&&e.attr("sizes",o),e.attr("src",i).removeAttr("data-lazy data-srcset data-sizes").removeClass("slick-loading"),!0===s.options.adaptiveHeight&&s.setPosition(),s.$slider.trigger("lazyLoaded",[s,e,i]),s.progressiveLazyLoad()},r.onerror=function(){t<3?setTimeout(function(){s.progressiveLazyLoad(t+1)},500):(e.removeAttr("data-lazy").removeClass("slick-loading").addClass("slick-lazyload-error"),s.$slider.trigger("lazyLoadError",[s,e,i]),s.progressiveLazyLoad())},r.src=i):s.$slider.trigger("allImagesLoaded",[s])},s.prototype.refresh=function(t){var e=this,i=e.slideCount-e.options.slidesToShow;!e.options.infinite&&e.currentSlide>i&&(e.currentSlide=i),e.slideCount<=e.options.slidesToShow&&(e.currentSlide=0),i=e.currentSlide,e.destroy(!0),c.extend(e,e.initials,{currentSlide:i}),e.init(),t||e.changeSlide({data:{message:"index",index:i}},!1)},s.prototype.registerBreakpoints=function(){var t,e,i,n=this,o=n.options.responsive||null;if("array"===c.type(o)&&o.length){for(t in n.respondTo=n.options.respondTo||"window",o)if(i=n.breakpoints.length-1,o.hasOwnProperty(t)){for(e=o[t].breakpoint;0<=i;)n.breakpoints[i]&&n.breakpoints[i]===e&&n.breakpoints.splice(i,1),i--;n.breakpoints.push(e),n.breakpointSettings[e]=o[t].settings}n.breakpoints.sort(function(t,e){return n.options.mobileFirst?t-e:e-t})}},s.prototype.reinit=function(){var t=this;t.$slides=t.$slideTrack.children(t.options.slide).addClass("slick-slide"),t.slideCount=t.$slides.length,t.currentSlide>=t.slideCount&&0!==t.currentSlide&&(t.currentSlide=t.currentSlide-t.options.slidesToScroll),t.slideCount<=t.options.slidesToShow&&(t.currentSlide=0),t.registerBreakpoints(),t.setProps(),t.setupInfinite(),t.buildArrows(),t.updateArrows(),t.initArrowEvents(),t.buildDots(),t.updateDots(),t.initDotEvents(),t.cleanUpSlideEvents(),t.initSlideEvents(),t.checkResponsive(!1,!0),!0===t.options.focusOnSelect&&c(t.$slideTrack).children().on("click.slick",t.selectHandler),t.setSlideClasses("number"==typeof t.currentSlide?t.currentSlide:0),t.setPosition(),t.focusHandler(),t.paused=!t.options.autoplay,t.autoPlay(),t.$slider.trigger("reInit",[t])},s.prototype.resize=function(){var t=this;c(window).width()!==t.windowWidth&&(clearTimeout(t.windowDelay),t.windowDelay=window.setTimeout(function(){t.windowWidth=c(window).width(),t.checkResponsive(),t.unslicked||t.setPosition()},50))},s.prototype.removeSlide=s.prototype.slickRemove=function(t,e,i){var n=this;if(t="boolean"==typeof t?!0===(e=t)?0:n.slideCount-1:!0===e?--t:t,n.slideCount<1||t<0||t>n.slideCount-1)return!1;n.unload(),(!0===i?n.$slideTrack.children():n.$slideTrack.children(this.options.slide).eq(t)).remove(),n.$slides=n.$slideTrack.children(this.options.slide),n.$slideTrack.children(this.options.slide).detach(),n.$slideTrack.append(n.$slides),n.$slidesCache=n.$slides,n.reinit()},s.prototype.setCSS=function(t){var e,i,n=this,o={};!0===n.options.rtl&&(t=-t),e="left"==n.positionProp?Math.ceil(t)+"px":"0px",i="top"==n.positionProp?Math.ceil(t)+"px":"0px",o[n.positionProp]=t,!1!==n.transformsEnabled&&(!(o={})===n.cssTransitions?o[n.animType]="translate("+e+", "+i+")":o[n.animType]="translate3d("+e+", "+i+", 0px)"),n.$slideTrack.css(o)},s.prototype.setDimensions=function(){var t=this,e=(!1===t.options.vertical?!0===t.options.centerMode&&t.$list.css({padding:"0px "+t.options.centerPadding}):(t.$list.height(t.$slides.first().outerHeight(!0)*t.options.slidesToShow),!0===t.options.centerMode&&t.$list.css({padding:t.options.centerPadding+" 0px"})),t.listWidth=t.$list.width(),t.listHeight=t.$list.height(),!1===t.options.vertical&&!1===t.options.variableWidth?(t.slideWidth=Math.ceil(t.listWidth/t.options.slidesToShow),t.$slideTrack.width(Math.ceil(t.slideWidth*t.$slideTrack.children(".slick-slide").length))):!0===t.options.variableWidth?t.$slideTrack.width(5e3*t.slideCount):(t.slideWidth=Math.ceil(t.listWidth),t.$slideTrack.height(Math.ceil(t.$slides.first().outerHeight(!0)*t.$slideTrack.children(".slick-slide").length))),t.$slides.first().outerWidth(!0)-t.$slides.first().width());!1===t.options.variableWidth&&t.$slideTrack.children(".slick-slide").width(t.slideWidth-e)},s.prototype.setFade=function(){var i,n=this;n.$slides.each(function(t,e){i=n.slideWidth*t*-1,!0===n.options.rtl?c(e).css({position:"relative",right:i,top:0,zIndex:n.options.zIndex-2,opacity:0}):c(e).css({position:"relative",left:i,top:0,zIndex:n.options.zIndex-2,opacity:0})}),n.$slides.eq(n.currentSlide).css({zIndex:n.options.zIndex-1,opacity:1})},s.prototype.setHeight=function(){var t;1===this.options.slidesToShow&&!0===this.options.adaptiveHeight&&!1===this.options.vertical&&(t=this.$slides.eq(this.currentSlide).outerHeight(!0),this.$list.css("height",t))},s.prototype.setOption=s.prototype.slickSetOption=function(){var t,e,i,n,o,s=this,r=!1;if("object"===c.type(arguments[0])?(i=arguments[0],r=arguments[1],o="multiple"):"string"===c.type(arguments[0])&&(i=arguments[0],n=arguments[1],r=arguments[2],"responsive"===arguments[0]&&"array"===c.type(arguments[1])?o="responsive":void 0!==arguments[1]&&(o="single")),"single"===o)s.options[i]=n;else if("multiple"===o)c.each(i,function(t,e){s.options[t]=e});else if("responsive"===o)for(e in n)if("array"!==c.type(s.options.responsive))s.options.responsive=[n[e]];else{for(t=s.options.responsive.length-1;0<=t;)s.options.responsive[t].breakpoint===n[e].breakpoint&&s.options.responsive.splice(t,1),t--;s.options.responsive.push(n[e])}r&&(s.unload(),s.reinit())},s.prototype.setPosition=function(){this.setDimensions(),this.setHeight(),!1===this.options.fade?this.setCSS(this.getLeft(this.currentSlide)):this.setFade(),this.$slider.trigger("setPosition",[this])},s.prototype.setProps=function(){var t=this,e=document.body.style;t.positionProp=!0===t.options.vertical?"top":"left","top"===t.positionProp?t.$slider.addClass("slick-vertical"):t.$slider.removeClass("slick-vertical"),void 0===e.WebkitTransition&&void 0===e.MozTransition&&void 0===e.msTransition||!0===t.options.useCSS&&(t.cssTransitions=!0),t.options.fade&&("number"==typeof t.options.zIndex?t.options.zIndex<3&&(t.options.zIndex=3):t.options.zIndex=t.defaults.zIndex),void 0!==e.OTransform&&(t.animType="OTransform",t.transformType="-o-transform",t.transitionType="OTransition",void 0===e.perspectiveProperty)&&void 0===e.webkitPerspective&&(t.animType=!1),void 0!==e.MozTransform&&(t.animType="MozTransform",t.transformType="-moz-transform",t.transitionType="MozTransition",void 0===e.perspectiveProperty)&&void 0===e.MozPerspective&&(t.animType=!1),void 0!==e.webkitTransform&&(t.animType="webkitTransform",t.transformType="-webkit-transform",t.transitionType="webkitTransition",void 0===e.perspectiveProperty)&&void 0===e.webkitPerspective&&(t.animType=!1),void 0!==e.msTransform&&(t.animType="msTransform",t.transformType="-ms-transform",t.transitionType="msTransition",void 0===e.msTransform)&&(t.animType=!1),void 0!==e.transform&&!1!==t.animType&&(t.animType="transform",t.transformType="transform",t.transitionType="transition"),t.transformsEnabled=t.options.useTransform&&null!==t.animType&&!1!==t.animType},s.prototype.setSlideClasses=function(t){var e,i,n,o=this,s=o.$slider.find(".slick-slide").removeClass("slick-active slick-center slick-current").attr("aria-hidden","true");o.$slides.eq(t).addClass("slick-current"),!0===o.options.centerMode?(i=o.options.slidesToShow%2==0?1:0,n=Math.floor(o.options.slidesToShow/2),!0===o.options.infinite&&((n<=t&&t<=o.slideCount-1-n?o.$slides.slice(t-n+i,t+n+1):(e=o.options.slidesToShow+t,s.slice(e-n+1+i,e+n+2))).addClass("slick-active").attr("aria-hidden","false"),0===t?s.eq(s.length-1-o.options.slidesToShow).addClass("slick-center"):t===o.slideCount-1&&s.eq(o.options.slidesToShow).addClass("slick-center")),o.$slides.eq(t).addClass("slick-center")):(0<=t&&t<=o.slideCount-o.options.slidesToShow?o.$slides.slice(t,t+o.options.slidesToShow):s.length<=o.options.slidesToShow?s:(i=o.slideCount%o.options.slidesToShow,e=!0===o.options.infinite?o.options.slidesToShow+t:t,o.options.slidesToShow==o.options.slidesToScroll&&o.slideCount-t<o.options.slidesToShow?s.slice(e-(o.options.slidesToShow-i),e+i):s.slice(e,e+o.options.slidesToShow))).addClass("slick-active").attr("aria-hidden","false"),"ondemand"!==o.options.lazyLoad&&"anticipated"!==o.options.lazyLoad||o.lazyLoad()},s.prototype.setupInfinite=function(){var t,e,i,n=this;if(!0===n.options.fade&&(n.options.centerMode=!1),!0===n.options.infinite&&!1===n.options.fade&&(e=null,n.slideCount>n.options.slidesToShow)){for(i=!0===n.options.centerMode?n.options.slidesToShow+1:n.options.slidesToShow,t=n.slideCount;t>n.slideCount-i;--t)c(n.$slides[e=t-1]).clone(!0).attr("id","").attr("data-slick-index",e-n.slideCount).prependTo(n.$slideTrack).addClass("slick-cloned");for(t=0;t<i+n.slideCount;t+=1)e=t,c(n.$slides[e]).clone(!0).attr("id","").attr("data-slick-index",e+n.slideCount).appendTo(n.$slideTrack).addClass("slick-cloned");n.$slideTrack.find(".slick-cloned").find("[id]").each(function(){c(this).attr("id","")})}},s.prototype.interrupt=function(t){t||this.autoPlay(),this.interrupted=t},s.prototype.selectHandler=function(t){t=c(t.target).is(".slick-slide")?c(t.target):c(t.target).parents(".slick-slide"),t=(t=parseInt(t.attr("data-slick-index")))||0;this.slideCount<=this.options.slidesToShow?this.slideHandler(t,!1,!0):this.slideHandler(t)},s.prototype.slideHandler=function(t,e,i){var n,o,s,r=this;e=e||!1,!0===r.animating&&!0===r.options.waitForAnimate||!0===r.options.fade&&r.currentSlide===t||(!1===e&&r.asNavFor(t),n=t,e=r.getLeft(n),s=r.getLeft(r.currentSlide),r.currentLeft=null===r.swipeLeft?s:r.swipeLeft,!1===r.options.infinite&&!1===r.options.centerMode&&(t<0||t>r.getDotCount()*r.options.slidesToScroll)||!1===r.options.infinite&&!0===r.options.centerMode&&(t<0||t>r.slideCount-r.options.slidesToScroll)?!1===r.options.fade&&(n=r.currentSlide,!0!==i?r.animateSlide(s,function(){r.postSlide(n)}):r.postSlide(n)):(r.options.autoplay&&clearInterval(r.autoPlayTimer),o=n<0?r.slideCount%r.options.slidesToScroll!=0?r.slideCount-r.slideCount%r.options.slidesToScroll:r.slideCount+n:n>=r.slideCount?r.slideCount%r.options.slidesToScroll!=0?0:n-r.slideCount:n,r.animating=!0,r.$slider.trigger("beforeChange",[r,r.currentSlide,o]),t=r.currentSlide,r.currentSlide=o,r.setSlideClasses(r.currentSlide),r.options.asNavFor&&(s=(s=r.getNavTarget()).slick("getSlick")).slideCount<=s.options.slidesToShow&&s.setSlideClasses(r.currentSlide),r.updateDots(),r.updateArrows(),!0===r.options.fade?(!0!==i?(r.fadeSlideOut(t),r.fadeSlide(o,function(){r.postSlide(o)})):r.postSlide(o),r.animateHeight()):!0!==i?r.animateSlide(e,function(){r.postSlide(o)}):r.postSlide(o)))},s.prototype.startLoad=function(){var t=this;!0===t.options.arrows&&t.slideCount>t.options.slidesToShow&&(t.$prevArrow.hide(),t.$nextArrow.hide()),!0===t.options.dots&&t.slideCount>t.options.slidesToShow&&t.$dots.hide(),t.$slider.addClass("slick-loading")},s.prototype.swipeDirection=function(){var t=this.touchObject.startX-this.touchObject.curX,e=this.touchObject.startY-this.touchObject.curY,e=Math.atan2(e,t);return(t=(t=Math.round(180*e/Math.PI))<0?360-Math.abs(t):t)<=45&&0<=t||t<=360&&315<=t?!1===this.options.rtl?"left":"right":135<=t&&t<=225?!1===this.options.rtl?"right":"left":!0===this.options.verticalSwiping?35<=t&&t<=135?"down":"up":"vertical"},s.prototype.swipeEnd=function(t){var e,i,n=this;if(n.dragging=!1,n.swiping=!1,n.scrolling)return n.scrolling=!1;if(n.interrupted=!1,n.shouldClick=!(10<n.touchObject.swipeLength),void 0===n.touchObject.curX)return!1;if(!0===n.touchObject.edgeHit&&n.$slider.trigger("edge",[n,n.swipeDirection()]),n.touchObject.swipeLength>=n.touchObject.minSwipe){switch(i=n.swipeDirection()){case"left":case"down":e=n.options.swipeToSlide?n.checkNavigable(n.currentSlide+n.getSlideCount()):n.currentSlide+n.getSlideCount(),n.currentDirection=0;break;case"right":case"up":e=n.options.swipeToSlide?n.checkNavigable(n.currentSlide-n.getSlideCount()):n.currentSlide-n.getSlideCount(),n.currentDirection=1}"vertical"!=i&&(n.slideHandler(e),n.touchObject={},n.$slider.trigger("swipe",[n,i]))}else n.touchObject.startX!==n.touchObject.curX&&(n.slideHandler(n.currentSlide),n.touchObject={})},s.prototype.swipeHandler=function(t){var e=this;if(!(!1===e.options.swipe||"ontouchend"in document&&!1===e.options.swipe||!1===e.options.draggable&&-1!==t.type.indexOf("mouse")))switch(e.touchObject.fingerCount=t.originalEvent&&void 0!==t.originalEvent.touches?t.originalEvent.touches.length:1,e.touchObject.minSwipe=e.listWidth/e.options.touchThreshold,!0===e.options.verticalSwiping&&(e.touchObject.minSwipe=e.listHeight/e.options.touchThreshold),t.data.action){case"start":e.swipeStart(t);break;case"move":e.swipeMove(t);break;case"end":e.swipeEnd(t)}},s.prototype.swipeMove=function(t){var e,i,n=this,o=void 0!==t.originalEvent?t.originalEvent.touches:null;return!(!n.dragging||n.scrolling||o&&1!==o.length)&&(e=n.getLeft(n.currentSlide),n.touchObject.curX=void 0!==o?o[0].pageX:t.clientX,n.touchObject.curY=void 0!==o?o[0].pageY:t.clientY,n.touchObject.swipeLength=Math.round(Math.sqrt(Math.pow(n.touchObject.curX-n.touchObject.startX,2))),o=Math.round(Math.sqrt(Math.pow(n.touchObject.curY-n.touchObject.startY,2))),!n.options.verticalSwiping&&!n.swiping&&4<o?!(n.scrolling=!0):(!0===n.options.verticalSwiping&&(n.touchObject.swipeLength=o),o=n.swipeDirection(),void 0!==t.originalEvent&&4<n.touchObject.swipeLength&&(n.swiping=!0,t.preventDefault()),t=(!1===n.options.rtl?1:-1)*(n.touchObject.curX>n.touchObject.startX?1:-1),!0===n.options.verticalSwiping&&(t=n.touchObject.curY>n.touchObject.startY?1:-1),i=n.touchObject.swipeLength,(n.touchObject.edgeHit=!1)===n.options.infinite&&(0===n.currentSlide&&"right"===o||n.currentSlide>=n.getDotCount()&&"left"===o)&&(i=n.touchObject.swipeLength*n.options.edgeFriction,n.touchObject.edgeHit=!0),!1===n.options.vertical?n.swipeLeft=e+i*t:n.swipeLeft=e+i*(n.$list.height()/n.listWidth)*t,!0===n.options.verticalSwiping&&(n.swipeLeft=e+i*t),!0!==n.options.fade&&!1!==n.options.touchMove&&(!0===n.animating?(n.swipeLeft=null,!1):void n.setCSS(n.swipeLeft))))},s.prototype.swipeStart=function(t){var e,i=this;if(i.interrupted=!0,1!==i.touchObject.fingerCount||i.slideCount<=i.options.slidesToShow)return!(i.touchObject={});void 0!==t.originalEvent&&void 0!==t.originalEvent.touches&&(e=t.originalEvent.touches[0]),i.touchObject.startX=i.touchObject.curX=void 0!==e?e.pageX:t.clientX,i.touchObject.startY=i.touchObject.curY=void 0!==e?e.pageY:t.clientY,i.dragging=!0},s.prototype.unfilterSlides=s.prototype.slickUnfilter=function(){null!==this.$slidesCache&&(this.unload(),this.$slideTrack.children(this.options.slide).detach(),this.$slidesCache.appendTo(this.$slideTrack),this.reinit())},s.prototype.unload=function(){var t=this;c(".slick-cloned",t.$slider).remove(),t.$dots&&t.$dots.remove(),t.$prevArrow&&t.htmlExpr.test(t.options.prevArrow)&&t.$prevArrow.remove(),t.$nextArrow&&t.htmlExpr.test(t.options.nextArrow)&&t.$nextArrow.remove(),t.$slides.removeClass("slick-slide slick-active slick-visible slick-current").attr("aria-hidden","true").css("width","")},s.prototype.unslick=function(t){this.$slider.trigger("unslick",[this,t]),this.destroy()},s.prototype.updateArrows=function(){var t=this;Math.floor(t.options.slidesToShow/2),!0===t.options.arrows&&t.slideCount>t.options.slidesToShow&&!t.options.infinite&&(t.$prevArrow.removeClass("slick-disabled").attr("aria-disabled","false"),t.$nextArrow.removeClass("slick-disabled").attr("aria-disabled","false"),0===t.currentSlide?(t.$prevArrow.addClass("slick-disabled").attr("aria-disabled","true"),t.$nextArrow.removeClass("slick-disabled").attr("aria-disabled","false")):(t.currentSlide>=t.slideCount-t.options.slidesToShow&&!1===t.options.centerMode||t.currentSlide>=t.slideCount-1&&!0===t.options.centerMode)&&(t.$nextArrow.addClass("slick-disabled").attr("aria-disabled","true"),t.$prevArrow.removeClass("slick-disabled").attr("aria-disabled","false")))},s.prototype.updateDots=function(){null!==this.$dots&&(this.$dots.find("li").removeClass("slick-active").end(),this.$dots.find("li").eq(Math.floor(this.currentSlide/this.options.slidesToScroll)).addClass("slick-active"))},s.prototype.visibility=function(){this.options.autoplay&&(document[this.hidden]?this.interrupted=!0:this.interrupted=!1)},c.fn.slick=function(){for(var t,e=arguments[0],i=Array.prototype.slice.call(arguments,1),n=this.length,o=0;o<n;o++)if("object"==typeof e||void 0===e?this[o].slick=new s(this[o],e):t=this[o].slick[e].apply(this[o].slick,i),void 0!==t)return t;return this}});var isOldIE=$("html").is(".lt-ie9"),Portal={MenuOpened:!1,MobileVerifier:{IsMobile:!1,IsTablet:!1,IsDesktop:!1,DetectMobile:function(t){var e={detectMobileBrowsers:{fullPattern:/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i,shortPattern:/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i}};return e.detectMobileBrowsers.fullPattern.test(t)||e.detectMobileBrowsers.shortPattern.test(t.substr(0,4))},DetectTablet:function(t){var e={detectMobileBrowsers:{tabletPattern:/android|ipad|playbook|silk/i}};return e.detectMobileBrowsers.tabletPattern.test(t)},Detect:function(){Portal.MobileVerifier.DetectMobile(navigator.userAgent)&&(Portal.MobileVerifier.IsMobile=!0),Portal.MobileVerifier.DetectTablet(navigator.userAgent)&&(Portal.MobileVerifier.IsTablet=!0),990<$(window).innerWidth()&&(Portal.MobileVerifier.IsDesktop=!0)}},Init:function(){Portal.MobileVerifier.Detect(),!Portal.MobileVerifier.IsDesktop||Portal.MobileVerifier.IsMobile||Portal.MobileVerifier.IsTablet||$(".showTooltip").each(function(){var t=$(this).data(),e=void 0!==t.container?t.container:"body",t=void 0!==t.placement?t.placement:"top";$(this).tooltip({container:e,placement:t})}),$(".mainNav .menu").on("click",function(t){t.preventDefault();var t=$(this),e=t.data("rel");(e=$(e,".mainNav")).is(":visible")?(t.removeClass("opened"),e.slideUp("fast",function(){Portal.MenuOpened=!1})):(t.addClass("opened"),e.slideDown("fast",function(){Portal.MenuOpened=!0}))}),$(".scielo__menu").on("click",function(t){var e=document.querySelector(".scielo-ico-menu"),i=document.querySelector(".scielo-ico-menu-opened"),n=document.querySelector(".scielo__mainMenu");this.classList.contains("opened")?(this.classList.remove("opened"),e.style.display="inline-block",i.style.display="none",n.animate([{top:"0px"},{top:"-1000px"}],{duration:600,iterations:1}),n.style.top="-1000px",Portal.MenuOpened=!1):(this.classList.add("opened"),e.style.display="none",i.style.display="inline-block",n.animate([{top:"-500px"},{top:"0px"}],{duration:100,iterations:1}),n.style.top="0px",Portal.MenuOpened=!0)}),$(".scielo-slider").slick({focusOnChange:!1,slidesToShow:4,infinite:!1,centerPadding:"0",arrows:!0,dots:!0,slidesToScroll:4,responsive:[{breakpoint:767,settings:{dots:!0,slidesToShow:1,slidesToScroll:1,centerMode:!1,arrows:!1,focusOnChange:!1}},{breakpoint:992,settings:{dots:!0,slidesToShow:2,slidesToScroll:2,centerMode:!1,arrows:!1,focusOnChange:!1}}]}),$(".scielo-slider").each(function(){$(this).data("autoheight")&&Portal.AdjustCardHeight($(this))}),$(".shareFacebook,.shareTwitter,.shareDelicious,.shareGooglePlus,.shareLinkedIn,.shareReddit,.shareStambleUpon,.shareCiteULike,.shareMendeley").on("click",function(t){t.preventDefault();var t=escape(this.href),e="https://www.facebook.com/sharer/sharer.php?u=",i="https://twitter.com/intent/tweet?text=",n="https://delicious.com/save?url=",o="https://plus.google.com/share?url=",s="http://www.linkedin.com/shareArticle?mini=true&url=",r="http://www.reddit.com/submit?url=",a="http://www.stumbleupon.com/submit?url=",l="http://www.citeulike.org/posturl?url=",c="http://www.mendeley.com/import/?url=",d="";$(this).is(".shareFacebook")?d=e+t:$(this).is(".shareTwitter")?d=i+t:$(this).is(".shareDelicious")?d=n+t:$(this).is(".shareGooglePlus")?d=o+t:$(this).is(".shareLinkedIn")?d=s+t:$(this).is(".shareReddit")?d=r+t:$(this).is(".shareStambleUpon")?d=a+t:$(this).is(".shareCiteULike")?d=l+t:$(this).is(".shareMendeley")&&(d=c+t),window.open(d,"share")}),$(".slider").each(Portal.Slider),$(".share_modal_id").on("click",function(t){t.preventDefault();t=$(location).attr("href");$.get("/form_mail/",{url:t},function(t){$("#share_modal_id").html(t),$("#share_modal_id").modal("show")})}),$(".contact_modal_id").on("click",function(t){t.preventDefault();$(location).attr("href");$.get($(this).data("url"),function(t){$("#contact_modal_id").html(t),$("#contact_modal_id").modal("show")})}),$(".floatingBtnError, .floatingBtnErrorHamburguerMenu").on("click",function(t){t.preventDefault();t=$(location).attr("href");$.get("/error_mail/",{url:t},function(t){$("#error_modal_id").html(t),$("#error_modal_id").modal("show")})}),$("#share_modal_id").on("hidden.bs.modal",function(){$(this).empty()}),$("#error_modal_id").on("hidden.bs.modal",function(){$(this).empty()}),$(".alternativeHeader").each(function(){var t=$(".mainMenu nav ul").html();$(this).find("nav ul").html(t)});for(var e=$("header").outerHeight(),t=($(window).on("scroll",function(){var t=0<window.scrollY?window.scrollY:0<window.pageYOffset?window.pageYOffset:document.documentElement.scrollTop;e<t?$(".alternativeHeader").stop(!0,!1).animate({top:"0"},200,function(){$("#mainMenu").is(":visible")&&$(".menu:eq(0)").trigger("click")}):$(".alternativeHeader").stop(!0,!1).animate({top:"-65px"},200,function(){$(this).find(".mainMenu").is(":visible")&&$(this).find(".menu").trigger("click")})}).on("keydown",function(t){27==t.keyCode&&$("a.scielo__menu").is(".opened")&&$("a.scielo__menu").trigger("click")}),$(".expandCollapseContent").on("click",function(t){t.preventDefault();var e=$("#issueIndex"),i=$("#issueData"),n=this;e.css("float","right"),i.is(":visible")?i.fadeOut("fast",function(){e.animate({width:"100%"},300),$(n).find(".glyphBtn").removeClass("opened").addClass("closed")}):e.animate({width:"75%"},300,function(){i.fadeIn("fast"),$(n).find(".glyphBtn").removeClass("closed").addClass("opened")}),$(n).tooltip("hide")}),$(".collapse-title").on("click",function(){var t=$(this),e=$(".collapse-content");e.is(":visible")?(e.slideUp("fast"),t.addClass("closed")):(e.slideDown("fast"),t.removeClass("closed"))}),$(".goto").on("click",function(t){t.preventDefault();t=(t=$(this).attr("href")).replace("#",""),t=$("a[name="+t+"]").offset();$("html,body").animate({scrollTop:t.top-60},500)}),$(".trigger").on("click",function(t){t.preventDefault();t=$(this).data("rel");$(t).click()}),$("input[name='link-share']").focus(function(){$(this).select(),window.clipboardData&&clipboardData.setData&&clipboardData.setData("text",$(this).text())}).mouseup(function(t){t.preventDefault()}),$(".levelMenu .dropdown-container").on("mouseenter mouseleave",function(t){var e=$(this).find(".dropdown-menu"),i=$(this).find(".dropdown-toggle");"mouseenter"==t.type?(e.show(),i.addClass("hover")):(e.hide(),i.removeClass("hover"))}),$(".nav-tabs a").click(function(t){t.preventDefault(),$(this).tab("show")}),$(".translateAction").on("click",function(t){t.preventDefault(),$("#translateArticleModal").modal("show")}),"undefined"!=typeof Clipboard&&new Clipboard(".copyLink").on("success",function(t){var e=$(t.trigger);e.addClass("copyFeedback"),setTimeout(function(){e.removeClass("copyFeedback")},2e3)}).on("error",function(t){console.error("Action:",t.action),console.error("Trigger:",t.trigger)}),$(".results .item")),i=0;i<t.length;i++){var n='<br class="visible-xs visible-sm"/><span class="socialLinks articleShareLink">\t\t\t\t\t<a href="" class="articleAction sendViaMail" data-toggle="tooltip" data-placement="top" title="Enviar link por e-mail">Enviar por e-mail</a>\t\t\t\t\t<a target="_blank" href="https://www.facebook.com/sharer/sharer.php?u='+(n=$(t[i]).parent()[0].children[i].children[1].children[0].children[0].href)+'" class="articleAction shareFacebook" data-toggle="tooltip" data-placement="top" title="Compartilhar no Facebook">Facebook</a>\t\t\t\t\t<a target="_blank" href="https://twitter.com/intent/tweet?text='+n+'" class="articleAction shareTwitter" data-toggle="tooltip" data-placement="top" title="Compartilhar no Twitter">Twitter</a>\t\t\t\t\t<a target="_blank" href="https://delicious.com/save?url='+n+'" class="articleAction shareDelicious" data-toggle="tooltip" data-placement="top" title="Compartilhar no Delicious">Delicious</a>\t\t\t\t\t<a href="" class="showTooltip dropdown-toggle" data-toggle="dropdown" data-placement="top" data-placement="top" title="Compartilhar em outras redes"><span class="glyphBtn otherNetworks"></span></a>\t\t\t\t\t<ul class="dropdown-menu articleShare">\t\t\t\t\t\t<li class="dropdown-header">Outras redes sociais</li>\t\t\t\t\t\t<li><a target="_blank" href="https://plus.google.com/share?url='+n+'" class="shareGooglePlus"><span class="glyphBtn googlePlus"></span> Google+</a></li>\t\t\t\t\t\t<li><a target="_blank" href="https://www.linkedin.com/shareArticle?mini=true&url='+n+'" class="shareLinkedIn"><span class="glyphBtn linkedIn"></span> LinkedIn</a></li>\t\t\t\t\t\t<li><a target="_blank" href="https://www.reddit.com/login?dest='+n+'" class="shareReddit"><span class="glyphBtn reddit"></span> Reddit</a></li>\t\t\t\t\t\t<li><a target="_blank" href="http://www.stumbleupon.com/submit?url='+n+'" class="shareStambleUpon"><span class="glyphBtn stambleUpon"></span> StambleUpon</a></li>\t\t\t\t\t\t<li><a target="_blank" href="http://www.citeulike.org/posturl?url='+n+'" class="shareCiteULike"><span class="glyphBtn citeULike"></span> CiteULike</a></li>\t\t\t\t\t\t<li><a target="_blank" href="https://www.mendeley.com/import/?url='+n+'" class="shareMendeley"><span class="glyphBtn mendeley"></span> Mendeley</a></li>\t\t\t\t\t</ul>\t\t\t\t</span>';$(".results .item:nth-child("+(i+1)+") div .line.articleResult").append(n)}$("#contactModal").on("show.bs.modal",function(t){t=$(t.relatedTarget).data("modal-title");$(this).find(".modal-title").text(t)}),$("#tst").length&&$("#tst").typeahead({order:"asc",minLength:3,dynamic:!0,delay:500,emptyTemplate:'Nenhum periódico encontrado para o termo: "{{query}}"',source:{journals:{display:"title",href:"{{link}}",ajax:function(t){return{type:"GET",url:"/journals/search/alpha/ajax/",contentType:"application/json",data:{query:"{{query}}",page:"1",query_filter:"current"},callback:{done:function(t){for(var e=[],i=0,n=t.journals.length;i<n;i++)e.push({title:t.journals[i].title,link:t.journals[i].links.detail});return console.log(e),e}}}}}}})},AdjustCardHeight(t){t=t.find(".card");let i=0;t.each(function(){var t=$(this).outerHeight(),e=parseFloat($(this).css("padding-bottom"));t>i&&(i=t-e)}),t.height(i)},Slider:function(){var t=$(this).attr("id"),t=$("#"+t),e=$(".slide-item",t),i=$(".slide-wrapper",t),n=$(".slide-back",t),o=$(".slide-next",t),s=$(".slide-container",t).outerWidth();warray=[],harray=[],e.each(function(){harray.push($(this).outerHeight()),warray.push($(this).outerWidth())}),itemProps={w:Math.max.apply(null,warray),h:Math.max.apply(null,harray)},wrapperWidth=e.length*itemProps.w+100,i.width(wrapperWidth),$(".slide-container",t).height(itemProps.h),n.css("top",itemProps.h/2+"px"),o.css("top",itemProps.h/2+"px"),n.hide(),i.width()<=t.width()?o.hide():o.show(),n.off().on("click",function(t){t.preventDefault();"auto"!=i.css("left")&&parseInt(i.css("left"));i.stop(!1,!0).animate({left:"+="+itemProps.w},100,function(){0==("auto"==i.css("left")?0:parseInt(i.css("left")))&&n.hide()}),o.show()}),o.off().on("click",function(t){t.preventDefault();"auto"!=i.css("left")&&parseInt(i.css("left"));i.stop(!1,!0).animate({left:"-="+itemProps.w},100,function(){("auto"==i.css("left")?0:parseInt(i.css("left")))<=-(wrapperWidth-s)&&o.hide()}),n.show()})}},SearchForm={SearchHistory:"",Init:function(){var p=".searchForm";$("input.trigger").off("click").on("click",function(){var cmd=$(this).data("rel");eval(cmd)}),$(p).on("submit",function(t){t.preventDefault();for(var e,i,n=$("*[name='q[]']",p),o=$("select[name='bool[]']",p),s=$("select[name='index[]']",p),r="",a=0,l=n.length;a<l;a++)""!=(e="iptQuery"==$(n[a]).attr("id")?$(n[a]).text():$(n[a]).val())&&(i=$("option:selected",s[a]).val(),1<=a&&(r+=" "+$("option:selected",o[a-1]).val()+" "),r+=""!=i?"("+i+":("+e+"))":2==l?e:"("+e+")");var t=$("input[name='collection']:checked",p).val(),t=(void 0!==t&&""!=t&&0==$("input[name='filter[in][]']").length&&(initial_filter_in=$("<input>").attr({type:"hidden",name:"filter[in][]",value:t}).appendTo("#searchForm")),$("input[name='publicationYear']:checked",p).val()),c=("1"==t?""!=(c=$("select[name='y1Start'] option:selected",p).val())&&(r+=" AND publication_year:["+c+" TO *]"):"2"==t&&(c=$("select[name='y2Start'] option:selected",p).val(),t=$("select[name='y2End'] option:selected",p).val(),""!=c)&&""!=t&&(r+=" AND publication_year:["+c+" TO "+t+"]"),""==(r=r.replace(/^AND|AND$|^OR|OR$/g,""))&&(r="*"),r=$.trim(r),document.searchForm.action,document.searchForm);return $("input[name='q']").val(r),c.submit(),!0}),$("textarea.form-control",p).on("keyup",SearchForm.TextareaAutoHeight).trigger("keyup").on("keypress",function(t){var e=$(this);""!=this.value?e.next().fadeIn("fast"):e.next().fadeOut("fast"),13!=t.which||t.shiftKey||$(p).submit()}),$("a.clearIptText",p).on("click",SearchForm.ClearPrevInput),$(".newSearchField",p).on("click",function(t){t.preventDefault(),SearchForm.InsertNewFieldRow(this,"#searchRow-matriz .searchRow",".searchForm .searchRow-container")}),$(".articleAction, .searchHistoryItem, .colActions .searchHistoryIcon",p).tooltip()},InsertSearchHistoryItem:function(t){var e=$(t).data("item"),i=$(t).parent().parent().find(".colSearch").text(),n=$("#iptQuery");n.append(' <div class="searchHistoryItem" contenteditable="false" data-toggle="tooltip" data-placement="top" title="'+i+'">#'+e+"</div> AND ").focus(),n.find(".searchHistoryItem").tooltip(),$(t).effect("transfer",{to:n.find(".searchHistoryItem:last-child")},1e3),SearchForm.PlaceCaretToEnd(document.getElementById("iptQuery"))},InsertNewFieldRow:function(t,e,i){t=$(t),e=$(e).clone(),i=$(i);var n=t.data("count");e.attr("id","searchRow-"+n),e.find(".eraseSearchField").data("rel",n),e.find(".eraseSearchField").on("click",function(t){t.preventDefault(),SearchForm.EraseFieldRow(this)}),""!=SearchForm.SearchHistory&&e.find("input[name='q[]']").on("focus",function(){SearchForm.SearchHistoryFocusIn(this)}).on("blur",function(){SearchForm.SearchHistoryFocusOut(this)}),e.appendTo(i).slideDown("fast"),e.find("textarea.form-control:visible").on("keyup",SearchForm.TextareaAutoHeight).trigger("keyup"),e.find("a.clearIptText").on("click",SearchForm.ClearPrevInput),e.find(".showTooltip").tooltip({container:"body"}),n=parseInt(n),t.data("count",++n)},TextareaAutoHeight:function(){""!=this.value?$(this).next("a").fadeIn("fast"):$(this).next("a").fadeOut("fast")},ClearPrevInput:function(){$(this).prev("input,textarea").val("").trigger("keyup")},EraseFieldRow:function(t){t=(t=$(t)).data("rel");$("#searchRow-"+t).slideUp("fast",function(){$(this).remove()})},CountCheckedResults:function(t,e){t=$(t);e=parseInt(t.data("preselected"))+parseInt($(e+":checked").length);t.text(e),0<e?t.addClass("highlighted"):t.removeClass("highlighted")},PlaceCaretToEnd:function(t){var e,i;t.focus(),void 0!==window.getSelection&&void 0!==document.createRange?((e=document.createRange()).selectNodeContents(t),e.collapse(!1),(i=window.getSelection()).removeAllRanges(),i.addRange(e)):void 0!==document.body.createTextRange&&((i=document.body.createTextRange()).moveToElementText(t),i.collapse(!1),i.select())},SubmitForm:function(t){var e=$(".searchForm").attr("action");$(".searchForm").attr("action",e+"?filter="+t).submit()}},Collection={Init:function(){var t=$(".collectionListStart .table-journal-list tbody tr"),s=$(".collectionListStart .table-journal-list tr td"),e=$(".collectionListStart .collectionSearch");$("#alpha .collectionListTotalInfo").text("(total "+t.length+")"),e.on("keyup",function(t){for(var e=t.target.value.toUpperCase(),i=0;i<s.length;i++){var n=s[i],o=n.textContent||n.innerText;o&&-1<o.toUpperCase().indexOf(e)?n.parentElement.style.display="":n.parentElement.style.display="none"}})},JournalListFinder:function(param,loading,container,labels,empty,scroll,callback,htmlFill){var currentPage=$(".collectionCurrentPage",container),totalPages=$(".collectionTotalPages",container),totalInfo=$(".collectionListTotalInfo",container),action=$(container).data("action");void 0!==htmlFill&&(loading=htmlFill.next(".collectionListLoading")),void 0===empty&&(empty=!1),param+="&page="+currentPage.val(),$.ajax({url:action,type:"POST",data:param,dataType:"json",beforeSend:function(){loading.show()}}).done(function(data){var ctt,ctt,ctt,ctt;loading.hide(),void 0!==data.journalList&&(-1==param.indexOf("&theme=")&&-1==param.indexOf("&publisher=")&&(totalInfo.html(labels[11].replace("{total}",data.total)),totalPages.val(data.totalPages)),ctt=Collection.JournalListFill(data,labels),(void 0!==htmlFill?(empty&&$(htmlFill).find("tbody").empty(),$(htmlFill)):(empty&&$(container).find("tbody").empty(),$(container))).find("tbody").append(ctt).find(".showTooltip").tooltip({container:"body"}),scroll)&&Collection.ScrollEvents(container,loading,labels),void 0!==data.themeList&&(totalInfo.html(labels[11].replace("{total}",data.total).replace("{totalTheme}",data.totalThemes)),ctt=Collection.ThemeListFill(data,labels,container.attr("id")),(void 0!==htmlFill?(empty&&$(htmlFill).find("tbody").empty(),$(htmlFill)):(empty&&$(container).find("tbody").empty(),$(container))).find("tbody").append(ctt).find(".showTooltip").tooltip({container:"body"}),Collection.CollapseEvents(container,labels)),void 0!==data.publisherList&&(totalInfo.html(labels[11].replace("{total}",data.total).replace("{totalPublisher}",data.totalPublisher)),ctt=Collection.PublisherListFill(data,labels,container.attr("id")),(void 0!==htmlFill?(empty&&$(htmlFill).find("tbody").empty(),$(htmlFill)):(empty&&$(container).find("tbody").empty(),$(container))).find("tbody").append(ctt).find(".showTooltip").tooltip({container:"body"}),Collection.CollapseEvents(container,labels)),void 0!==data.collectionList&&(totalInfo.html(labels[11].replace("{total}",data.total).replace("{totalCollection}",data.totalCollection)),ctt=Collection.CollectionListFill(data,labels,container.attr("id")),(void 0!==htmlFill?(empty&&$(htmlFill).find("tbody").empty(),$(htmlFill)):(empty&&$(container).find("tbody").empty(),$(container))).find("tbody").append(ctt).find(".showTooltip").tooltip({container:"body"})),void 0!==callback&&eval(callback)}).error(function(t){loading.hide(),console.warn("Error #001: Error found on loading journal list")})},JournalListFill:function(t,e){for(var i="",n=0,o=t.journalList.length;n<o;n++){var s=t.journalList[n];s.Last=s.Last.split(";"),s.Publisher=s.Publisher.split(";"),i+='\t\t\t\t\t\t<tr>\t\t\t\t\t\t\t<td class="actions">\t\t\t\t\t\t\t\t<a href="'+s.Links[0]+'" class="showTooltip" title="'+e[5]+'"><span class="glyphBtn home"></span></a> \t\t\t\t\t\t\t\t<a href="'+s.Links[1]+'" class="showTooltip" title="'+e[6]+'"><span class="glyphBtn submission"></span></a> \t\t\t\t\t\t\t\t<a href="'+s.Links[2]+'" class="showTooltip" title="'+e[7]+'"><span class="glyphBtn authorInstructions"></span></a> \t\t\t\t\t\t\t\t<a href="'+s.Links[5]+'" class="showTooltip" title="'+e[12]+'"><span class="glyphBtn editorial"></span></a> \t\t\t\t\t\t\t\t<a href="'+s.Links[3]+'" class="showTooltip" title="'+e[8]+'"><span class="glyphBtn about"></span></a> \t\t\t\t\t\t\t\t<a href="'+s.Links[4]+'" class="showTooltip" title="'+e[9]+'"><span class="glyphBtn contact"></span></a> \t\t\t\t\t\t\t</td>\t\t\t\t\t\t\t<td>\t\t\t\t\t\t\t\t<a href="'+s.Links[0]+'" class="collectionLink '+(0==s.Active?"disabled":"")+'">\t\t\t\t\t\t\t\t\t<strong class="journalTitle">'+s.Journal+'</strong>,\t\t\t\t\t\t\t\t\t<strong class="journalIssues">'+s.Issues+" "+e[0]+"</strong>,\t\t\t\t\t\t\t\t\t"+e[1]+"\t\t\t\t\t\t\t\t\t"+(""!=s.Last[0]?'<span class="journalLastVolume"><em>'+e[2]+"</em> "+s.Last[0]+"</span>":"")+"\t\t\t\t\t\t\t\t\t"+(""!=s.Last[1]?'<span class="journalLastNumber"><em>'+e[3]+"</em> "+s.Last[1]+"</span>":"")+"\t\t\t\t\t\t\t\t\t"+(""!=s.Last[2]?'<span class="journalLastSuppl"><em>'+e[4]+"</em> "+s.Last[2]+"</span>":"")+"\t\t\t\t\t\t\t\t\t- \t\t\t\t\t\t\t\t\t"+(""!=s.Last[3]?'<span class="journalLastPubDate">'+s.Last[3]+"</span>":"")+" \t\t\t\t\t\t\t\t\t"+(0==s.Active?e[10]:"")+" \t\t\t\t\t\t\t\t</a>\t\t\t\t\t\t\t</td>",s.Collection&&(i+=' \t\t\t\t\t\t\t\t<td>\t\t\t\t\t\t\t\t\t<span class="glyphFlags '+s.Collection+'"></span> '+Collection.PortalCollectionNameFill(s.Collection)+"\t\t\t\t\t\t\t\t</td>"),i+="\t\t\t\t\t\t</tr>"}return i},PortalCollectionNameFill:function(t){var e="";if(window.collections)for(var i=0,n=window.collections.length;i<n;i++)window.collections[i].id==t&&(e=window.collections[i].name);return e},ThemeListFill:function(t,e,i){for(var n='\t<tr>\t\t\t\t\t\t\t<td class="collapseContainer">',o=0,s=t.themeList.length;o<s;o++){n+='\t\t<div class="themeItem">\t\t\t\t\t\t\t\t\t<a href="javascript:;" id="'+i+"-collapseTitle-"+o+'" \t\t\t\t\t\t\t\t\tclass="collapseTitleBlock '+(void 0===t.themeList[o].journalList?"closed":"")+'" data-id="'+t.themeList[o].id+'">\t\t\t\t\t\t\t\t\t\t<strong>'+t.themeList[o].Area+"</strong>\t\t\t\t\t\t\t\t\t\t("+t.themeList[o].Total+')\t\t\t\t\t\t\t\t\t</a> \t\t\t\t\t\t\t\t\t<div class="collapseContent" id="'+i+"-collapseContent-"+o+'" '+(void 0===t.themeList[o].journalList?'style="display: none;"':"")+">";for(var r=0,a=t.themeList[o].SubAreas.length;r<a;r++)n+='\t\t\t<a href="javascript:;" id="'+i+"-collapseTitle-"+o+"-sub-"+r+'" \t\t\t\t\t\t\t\t\t\t\tclass="collapseTitle '+(void 0===t.themeList[o].SubAreas[r].journalList?"closed":"")+'" data-id="'+t.themeList[o].SubAreas[r].id+'">\t\t\t\t\t\t\t\t\t\t\t<strong>'+t.themeList[o].SubAreas[r].Area+"</strong>\t\t\t\t\t\t\t\t\t\t\t\t("+t.themeList[o].SubAreas[r].Total+')\t\t\t\t\t\t\t\t\t\t</a>\t\t\t\t\t\t\t\t\t\t<div class="collapseContent" id="'+i+"-collapseContent-"+o+"-sub-"+r+'" '+(void 0===t.themeList[o].SubAreas[r].journalList?'style="display: none;"':"")+'>\t\t\t\t\t\t\t\t\t\t\t<table> \t\t\t\t\t\t\t\t\t\t\t\t<thead> \t\t\t\t\t\t\t\t\t\t\t\t\t<tr> \t\t\t\t\t\t\t\t\t\t\t\t\t\t<th class="actions"></th> \t\t\t\t\t\t\t\t\t\t\t\t\t\t<th>'+e[12]+"</th> \t\t\t\t\t\t\t\t\t\t\t\t\t\t"+(t.collection?"<th class='flags'>"+e[14]+"</th>":"")+" \t\t\t\t\t\t\t\t\t\t\t\t\t</tr> \t\t\t\t\t\t\t\t\t\t\t\t</thead> \t\t\t\t\t\t\t\t\t\t\t\t<tbody>",void 0!==t.themeList[o].SubAreas[r].journalList&&(n+=Collection.JournalListFill(t.themeList[o].SubAreas[r],e)),n+='\t\t\t\t\t\t</tbody>\t\t\t\t\t\t\t\t\t\t\t</table> \t\t\t\t\t\t\t\t\t\t</div> \t\t\t\t\t\t\t\t\t\t<div class="collapseContent collectionListLoading" style="display: none;"></div>\t\t\t\t\t\t\t\t\t\t';n+="\t\t\t</div>"}return n+="\t\t</td>\t\t\t\t\t\t</tr>"},PublisherListFill:function(t,e,i){for(var n='\t<tr>\t\t\t\t\t\t\t<td class="collapseContainer">',o=0,s=t.publisherList.length;o<s;o++)n+='\t\t<div class="themeItem">\t\t\t\t\t\t\t\t\t<a href="javascript:;" id="'+i+"-collapseTitle-"+o+'" class="collapseTitle '+(void 0===t.publisherList[o].journalList?"closed":"")+'"><strong>'+t.publisherList[o].Publisher+"</strong> ("+t.publisherList[o].Total+')</a> \t\t\t\t\t\t\t\t\t<div class="collapseContent" id="'+i+"-collapseContent-"+o+'" '+(void 0===t.publisherList[o].journalList?'style="display: none;"':"")+'> \t\t\t\t\t\t\t\t\t\t<table> \t\t\t\t\t\t\t\t\t\t\t<thead> \t\t\t\t\t\t\t\t\t\t\t\t<tr> \t\t\t\t\t\t\t\t\t\t\t\t\t<th class="actions"></th> \t\t\t\t\t\t\t\t\t\t\t\t\t<th>'+e[12]+"</th> \t\t\t\t\t\t\t\t\t\t\t\t\t"+(t.collection?"<th class='flags'>"+e[14]+"</th>":"")+" \t\t\t\t\t\t\t\t\t\t\t\t</tr> \t\t\t\t\t\t\t\t\t\t\t</thead> \t\t\t\t\t\t\t\t\t\t\t<tbody>",void 0!==t.publisherList[o].journalList&&(n+=Collection.JournalListFill(t.publisherList[o],e)),n+='\t\t\t\t\t</tbody> \t\t\t\t\t\t\t\t\t\t</table> \t\t\t\t\t\t\t\t\t</div> \t\t\t\t\t\t\t\t\t<div class="collapseContent collectionListLoading" style="display: none;"></div>\t\t\t\t\t\t\t\t</div>';return n+="\t\t</td>\t\t\t\t\t\t</tr>"},CollectionListFill:function(t,e,i){for(var n='\t<tr>\t\t\t\t\t\t\t<td class="collapseContainer">',o=0,s=t.collectionList.length;o<s;o++)n+='\t\t<div class="themeItem collectionBold">\t\t\t\t\t\t\t\t<a href="'+t.collectionList[o].link+'" class="collapseTitle closed" target="_blank"><span class="glyphFlags '+t.collectionList[o].id+'"></span> <strong>'+t.collectionList[o].name+"</strong> ("+t.collectionList[o].Total+")</a> \t\t\t\t\t\t\t</div>";return n+="\t\t</td>\t\t\t\t\t\t</tr>"},ScrollEvents:function(e,i,n){var o=$(".collectionCurrentPage",e),s=$(".collectionTotalPages",e),t=$(".collectionSearch",e),r=$(e).data("perpage"),a=($(e).data("method"),"method=alphabetic&rp="+r+(""!=t.val()?"&query="+t.val():""));$(window).off("scroll").on("scroll",function(){var t;o.val()<s.val()?($("footer").hide(),$(window).scrollTop()+$(window).height()==$(document).height()&&(t=parseInt(o.val()),o.val(++t),a+="&page="+t,Collection.JournalListFinder(a,i,e,n,!1,!1))):$("footer").show()})},CollapseEvents:function(o,s){var r=$(o).data("method"),t=$(".collectionSearch",o),a="method=alphabetic"+(""!=t.val()?"&query="+t.val():"");$(".collapseTitle,.collapseTitleBlock",o).on("click",function(){var t=$(this),e=t.parent().find(".collectionListLoading"),i=t.next(".collapseContent"),n=i.find("table tbody tr").length;i.is(":visible")?(i.slideUp("fast"),$(this).addClass("closed")):!t.is(".collapseTitleBlock")&&0==n?(n=void 0!==t.data("id")?t.data("id"):$("strong",this).text(),a+="&"+r+"="+n,Collection.JournalListFinder(a,e,o,s,!0,!1,"Collection.CollapseOpen('#"+i.attr("id")+"','#"+t.attr("id")+"')",i)):(i.slideDown("fast"),e.hide(),$(this).removeClass("closed"))})},CollapseOpen:function(t,e){$(t).slideDown("fast"),$(e).removeClass("closed")}},Validator=(Journal={Init:function(){$("#sortBy").change(function(){$("#sortBy option:selected").each(function(){Journal.publicationSort($(this).val())})}),$(".scroll").on("click",function(t){var e=$(this).attr("href").split("#")[1];0<$("a[name="+e+"]").length&&(e=$("a[name="+e+"]").offset(),$("html,body").animate({scrollTop:e.top+1},500))})},Bindings:function(t){},publicationSort:function(t){for(var e=$(".issueIndent>ul.articles"),i=e.length,n=0;n<=i;n++){var o=e[n],s=$(o).children();("YEAR_DESC"===t?$(s).sort(function(t,e){t=parseInt($(t).data("date"));return parseInt($(e).data("date"))<t?1:-1}):$(s).sort(function(t,e){return parseInt($(t).data("date"))<parseInt($(e).data("date"))?1:-1})).each(function(){$(o).append(this)})}},publicatorName:function(){var t=$(".namePlublisher").text();56<=t.length&&($(".namePlublisher").attr("data-toggle","tooltip"),$(".namePlublisher").attr("title",t))},checkPressReleases:function(){var i,n,t=parseInt($("#pressReleasesList").attr("data-lastdays")),e=new Date;isNaN(t)||t<=0||((i=new Date).setDate(e.getDate()-t),isNaN(i.getTime())?console.error("Data inválida ao calcular a data de "+t+" dias atrás."):(n=!1,$("#pressReleasesList .card").each(function(){var t=$(this).attr("data-publication-date"),e=new Date(t);if(!isNaN(e.getTime()))return i<=e?!(n=!0):void 0;console.error("Data inválida encontrada em data-publication-date:",t)}),n&&$("#pressReleasesList").removeClass("d-none")))}},{MultipleEmails:function(t,e){for(var e=e||";",i=/^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,n=!0,o=t.split(e),s=0;s<o.length;s++)o[s]=o[s].trim(),""!=o[s]&&0!=i.test(o[s])||(n=!1);return n}}),Cookie={Get:function(t,e){return void 0===e?e="":e+="/",t=e+t,0<document.cookie.length&&-1!=(c_start=document.cookie.indexOf(t+"="))?(c_start=c_start+t.length+1,-1==(c_end=document.cookie.indexOf(";",c_start))&&(c_end=document.cookie.length),unescape(document.cookie.substring(c_start,c_end))):""},Set:function(t,e,i,n){var o,i=void 0!==i?((o=new Date).setTime(o.getTime()+24*i*60*60*1e3),"; expires="+o.toGMTString()):"";void 0===n?n="":n+="/",""!=Cookie.Get(t)&&(document.cookie=n+t+"="+e+"expires=Thu, 01 Jan 1970 00:00:01 GMT; path=/"),document.cookie=n+t+"="+e+i+"; path=/"}},alertMessage=($(function(){var t;Portal.Init(),$(".searchForm").length&&SearchForm.Init(),$("body.journal").length&&Journal.Init(),Journal.checkPressReleases(),$("body.collection, body.portal").length&&Collection.Init(),$("body.portal.home").length&&$(".portal .twitter").twittie({dateFormat:"%d de %B",template:'<span>{{date}}</span><div class="info">{{tweet}}</div><div class="tShare"><a href="https://twitter.com/intent/tweet?in_reply_to={{twitterId}}" target="_blank" class="showTooltip" data-placement="top" title="" data-original-title="Compartilhar no Twitter"><span class="glyphBtn tShare"></span></a><a href="https://twitter.com/intent/retweet?tweet_id={{twitterId}}" target="_blank" class="showTooltip" data-placement="top" title="" data-original-title="Atualizar"><span class="glyphBtn tReload"></span></a><a href="https://twitter.com/intent/favorite?tweet_id={{twitterId}}" target="_blank" class="showTooltip" data-placement="top" title="" data-original-title="Adicionar as favoritos no Twitter"><span class="glyphBtn tFavorite"></span></a></div>',count:3,loadingText:"Carregando..."}),$(".portal .collectionList").length&&(t=window.location.hash),$('.portal .collection .nav-tabs a[href="'+t+'"]').tab("show"),$(".namePlublisher").length&&Journal.publicatorName()}),{isActive:!0,msgContainer:document.querySelector("div.alert.alert-warning.alert-dismissible"),setCookie:function(t,e,i){var n=new Date,i=(n.setTime(n.getTime()+24*i*60*60*1e3),"expires="+n.toUTCString());document.cookie=t+"="+e+";"+i+";path=/"},getCookie:function(t){for(var e=t+"=",i=decodeURIComponent(document.cookie).split(";"),n=0;n<i.length;n++){for(var o=i[n];" "==o.charAt(0);)o=o.substring(1);if(0==o.indexOf(e))return o.substring(e.length,o.length)}return""},checkCookie:function(t){t=alertMessage.getCookie(t);""==t||null==t?alertMessage.showAlertMessage():"object"==typeof msgContainer&&null!==msgContainer&&(alertMessage.msgContainer.style.display="none")},clearCookie:function(t){alertMessage.setCookie(t,"no",-365)},showAlertMessage:function(){var t,e;e="object"==typeof msgContainer&&null!==msgContainer?(t=alertMessage.msgContainer).querySelector("button.close"):t=null,null!==t&&null!==e&&(e.addEventListener("click",function(){alertMessage.setCookie("alert-message-accepted","yes",365)}),t.style.display="block")},Init:function(){alertMessage.isActive?alertMessage.checkCookie("alert-message-accepted"):alertMessage.clearCookie("alert-message-accepted")}}),ModalForms=(alertMessage.Init(),$(function(){$(".dropdown-toggle").dropdown(),$('[data-toggle="tooltip"], .dropdown-tooltip').tooltip(),$(".image").on("error",function(){this.src="/static/img/fallback_image.png"}),$(".collapseAbstractBlock").on("click",function(t){t.preventDefault(),$(".collapseAbstractBlock").each(function(t){var e=$(this);$("#"+e.data("id")).slideUp(),e.removeClass("opened")});var t=$(this),e=$("#"+t.data("id"));e.is(":visible")?(e.slideUp(),t.removeClass("opened")):(e.slideDown(),t.addClass("opened"))}),$(".modal").on("hidden.bs.modal",function(t){$(this).find("input[type=text], textarea, select").val("").end().find("input[type=checkbox], input[type=radio]").prop("checked","").end()})}),!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.moment=e()}(this,function(){"use strict";var N;function p(){return N.apply(null,arguments)}function u(t){return t instanceof Array||"[object Array]"===Object.prototype.toString.call(t)}function I(t){return null!=t&&"[object Object]"===Object.prototype.toString.call(t)}function h(t){return void 0===t}function H(t){return"number"==typeof t||"[object Number]"===Object.prototype.toString.call(t)}function F(t){return t instanceof Date||"[object Date]"===Object.prototype.toString.call(t)}function R(t,e){for(var i=[],n=0;n<t.length;++n)i.push(e(t[n],n));return i}function f(t,e){return Object.prototype.hasOwnProperty.call(t,e)}function m(t,e){for(var i in e)f(e,i)&&(t[i]=e[i]);return f(e,"toString")&&(t.toString=e.toString),f(e,"valueOf")&&(t.valueOf=e.valueOf),t}function c(t,e,i,n){return Ae(t,e,i,n,!0).utc()}function g(t){return null==t._pf&&(t._pf={empty:!1,unusedTokens:[],unusedInput:[],overflow:-2,charsLeftOver:0,nullInput:!1,invalidMonth:null,invalidFormat:!1,userInvalidated:!1,iso:!1,parsedDateParts:[],meridiem:null,rfc2822:!1,weekdayMismatch:!1}),t._pf}var q=Array.prototype.some||function(t){for(var e=Object(this),i=e.length>>>0,n=0;n<i;n++)if(n in e&&t.call(this,e[n],n,e))return!0;return!1};function Y(t){if(null==t._isValid){var e=g(t),i=q.call(e.parsedDateParts,function(t){return null!=t}),i=!isNaN(t._d.getTime())&&e.overflow<0&&!e.empty&&!e.invalidMonth&&!e.invalidWeekday&&!e.nullInput&&!e.invalidFormat&&!e.userInvalidated&&(!e.meridiem||e.meridiem&&i);if(t._strict&&(i=i&&0===e.charsLeftOver&&0===e.unusedTokens.length&&void 0===e.bigHour),null!=Object.isFrozen&&Object.isFrozen(t))return i;t._isValid=i}return t._isValid}function W(t){var e=c(NaN);return null!=t?m(g(e),t):g(e).userInvalidated=!0,e}var B=p.momentProperties=[];function z(t,e){var i,n,o;if(h(e._isAMomentObject)||(t._isAMomentObject=e._isAMomentObject),h(e._i)||(t._i=e._i),h(e._f)||(t._f=e._f),h(e._l)||(t._l=e._l),h(e._strict)||(t._strict=e._strict),h(e._tzm)||(t._tzm=e._tzm),h(e._isUTC)||(t._isUTC=e._isUTC),h(e._offset)||(t._offset=e._offset),h(e._pf)||(t._pf=g(e)),h(e._locale)||(t._locale=e._locale),0<B.length)for(i=0;i<B.length;i++)h(o=e[n=B[i]])||(t[n]=o);return t}var U=!1;function G(t){z(this,t),this._d=new Date(null!=t._d?t._d.getTime():NaN),this.isValid()||(this._d=new Date(NaN)),!1===U&&(U=!0,p.updateOffset(this),U=!1)}function y(t){return t instanceof G||null!=t&&null!=t._isAMomentObject}function a(t){return t<0?Math.ceil(t)||0:Math.floor(t)}function d(t){var t=+t,e=0;return e=0!=t&&isFinite(t)?a(t):e}function V(t,e,i){for(var n=Math.min(t.length,e.length),o=Math.abs(t.length-e.length),s=0,r=0;r<n;r++)(i&&t[r]!==e[r]||!i&&d(t[r])!==d(e[r]))&&s++;return s+o}function X(t){!1===p.suppressDeprecationWarnings&&"undefined"!=typeof console&&console.warn&&console.warn("Deprecation warning: "+t)}function t(o,s){var r=!0;return m(function(){if(null!=p.deprecationHandler&&p.deprecationHandler(null,o),r){for(var t,e=[],i=0;i<arguments.length;i++){if(t="","object"==typeof arguments[i]){for(var n in t+="\n["+i+"] ",arguments[0])t+=n+": "+arguments[0][n]+", ";t=t.slice(0,-2)}else t=arguments[i];e.push(t)}X(o+"\nArguments: "+Array.prototype.slice.call(e).join("")+"\n"+(new Error).stack),r=!1}return s.apply(this,arguments)},s)}var Z={};function Q(t,e){null!=p.deprecationHandler&&p.deprecationHandler(t,e),Z[t]||(X(e),Z[t]=!0)}function r(t){return t instanceof Function||"[object Function]"===Object.prototype.toString.call(t)}function J(t,e){var i,n=m({},t);for(i in e)f(e,i)&&(I(t[i])&&I(e[i])?(n[i]={},m(n[i],t[i]),m(n[i],e[i])):null!=e[i]?n[i]=e[i]:delete n[i]);for(i in t)f(t,i)&&!f(e,i)&&I(t[i])&&(n[i]=m({},n[i]));return n}function K(t){null!=t&&this.set(t)}p.suppressDeprecationWarnings=!1,p.deprecationHandler=null;var tt=Object.keys||function(t){var e,i=[];for(e in t)f(t,e)&&i.push(e);return i};var et={};function e(t,e){var i=t.toLowerCase();et[i]=et[i+"s"]=et[e]=t}function s(t){return"string"==typeof t?et[t]||et[t.toLowerCase()]:void 0}function it(t){var e,i,n={};for(i in t)f(t,i)&&(e=s(i))&&(n[e]=t[i]);return n}var nt={};function i(t,e){nt[t]=e}function ot(e,i){return function(t){return null!=t?(rt(this,e,t),p.updateOffset(this,i),this):st(this,e)}}function st(t,e){return t.isValid()?t._d["get"+(t._isUTC?"UTC":"")+e]():NaN}function rt(t,e,i){t.isValid()&&t._d["set"+(t._isUTC?"UTC":"")+e](i)}function l(t,e,i){var n=""+Math.abs(t);return(0<=t?i?"+":"":"-")+Math.pow(10,Math.max(0,e-n.length)).toString().substr(1)+n}var at=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,lt=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,ct={},dt={};function n(t,e,i,n){var o="string"==typeof n?function(){return this[n]()}:n;t&&(dt[t]=o),e&&(dt[e[0]]=function(){return l(o.apply(this,arguments),e[1],e[2])}),i&&(dt[i]=function(){return this.localeData().ordinal(o.apply(this,arguments),t)})}function ut(t,e){return t.isValid()?(e=ht(e,t.localeData()),ct[e]=ct[e]||function(n){for(var t,o=n.match(at),e=0,s=o.length;e<s;e++)dt[o[e]]?o[e]=dt[o[e]]:o[e]=(t=o[e]).match(/\[[\s\S]/)?t.replace(/^\[|\]$/g,""):t.replace(/\\/g,"");return function(t){for(var e="",i=0;i<s;i++)e+=r(o[i])?o[i].call(t,n):o[i];return e}}(e),ct[e](t)):t.localeData().invalidDate()}function ht(t,e){var i=5;function n(t){return e.longDateFormat(t)||t}for(lt.lastIndex=0;0<=i&<.test(t);)t=t.replace(lt,n),lt.lastIndex=0,--i;return t}var pt=/\d/,o=/\d\d/,ft=/\d{3}/,mt=/\d{4}/,v=/[+-]?\d{6}/,b=/\d\d?/,gt=/\d\d\d\d?/,yt=/\d\d\d\d\d\d?/,vt=/\d{1,3}/,bt=/\d{1,4}/,w=/[+-]?\d{1,6}/,wt=/\d+/,_t=/[+-]?\d+/,kt=/Z|[+-]\d\d:?\d\d/gi,xt=/Z|[+-]\d\d(?::?\d\d)?/gi,Tt=/[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i,St={};function _(t,i,n){St[t]=r(i)?i:function(t,e){return t&&n?n:i}}function Ct(t,e){return f(St,t)?St[t](e._strict,e._locale):new RegExp(Et(t.replace("\\","").replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g,function(t,e,i,n,o){return e||i||n||o})))}function Et(t){return t.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}var Dt={};function k(t,i){var e,n=i;for("string"==typeof t&&(t=[t]),H(i)&&(n=function(t,e){e[i]=d(t)}),e=0;e<t.length;e++)Dt[t[e]]=n}function At(t,o){k(t,function(t,e,i,n){i._w=i._w||{},o(t,i._w,i,n)})}var x=0,T=1,S=2,C=3,E=4,D=5,Lt=6,Ot=7,$t=8,Mt=Array.prototype.indexOf||function(t){for(var e=0;e<this.length;++e)if(this[e]===t)return e;return-1},A=Mt;function jt(t,e){return new Date(Date.UTC(t,e+1,0)).getUTCDate()}n("M",["MM",2],"Mo",function(){return this.month()+1}),n("MMM",0,0,function(t){return this.localeData().monthsShort(this,t)}),n("MMMM",0,0,function(t){return this.localeData().months(this,t)}),e("month","M"),i("month",8),_("M",b),_("MM",b,o),_("MMM",function(t,e){return e.monthsShortRegex(t)}),_("MMMM",function(t,e){return e.monthsRegex(t)}),k(["M","MM"],function(t,e){e[T]=d(t)-1}),k(["MMM","MMMM"],function(t,e,i,n){n=i._locale.monthsParse(t,n,i._strict);null!=n?e[T]=n:g(i).invalidMonth=t});var Pt=/D[oD]?(\[[^\[\]]*\]|\s)+MMMM?/,Mt="January_February_March_April_May_June_July_August_September_October_November_December".split("_");var Nt="Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_");function It(t,e){var i;if(t.isValid()){if("string"==typeof e)if(/^\d+$/.test(e))e=d(e);else if(!H(e=t.localeData().monthsParse(e)))return;i=Math.min(t.date(),jt(t.year(),e)),t._d["set"+(t._isUTC?"UTC":"")+"Month"](e,i)}}function Ht(t){return null!=t?(It(this,t),p.updateOffset(this,!0),this):st(this,"Month")}var Ft=Tt;var Rt=Tt;function qt(){function t(t,e){return e.length-t.length}for(var e,i=[],n=[],o=[],s=0;s<12;s++)e=c([2e3,s]),i.push(this.monthsShort(e,"")),n.push(this.months(e,"")),o.push(this.months(e,"")),o.push(this.monthsShort(e,""));for(i.sort(t),n.sort(t),o.sort(t),s=0;s<12;s++)i[s]=Et(i[s]),n[s]=Et(n[s]);for(s=0;s<24;s++)o[s]=Et(o[s]);this._monthsRegex=new RegExp("^("+o.join("|")+")","i"),this._monthsShortRegex=this._monthsRegex,this._monthsStrictRegex=new RegExp("^("+n.join("|")+")","i"),this._monthsShortStrictRegex=new RegExp("^("+i.join("|")+")","i")}function Yt(t){return Wt(t)?366:365}function Wt(t){return t%4==0&&t%100!=0||t%400==0}n("Y",0,0,function(){var t=this.year();return t<=9999?""+t:"+"+t}),n(0,["YY",2],0,function(){return this.year()%100}),n(0,["YYYY",4],0,"year"),n(0,["YYYYY",5],0,"year"),n(0,["YYYYYY",6,!0],0,"year"),e("year","y"),i("year",1),_("Y",_t),_("YY",b,o),_("YYYY",bt,mt),_("YYYYY",w,v),_("YYYYYY",w,v),k(["YYYYY","YYYYYY"],x),k("YYYY",function(t,e){e[x]=2===t.length?p.parseTwoDigitYear(t):d(t)}),k("YY",function(t,e){e[x]=p.parseTwoDigitYear(t)}),k("Y",function(t,e){e[x]=parseInt(t,10)}),p.parseTwoDigitYear=function(t){return d(t)+(68<d(t)?1900:2e3)};var Bt=ot("FullYear",!0);function zt(t,e,i,n,o,s,r){e=new Date(t,e,i,n,o,s,r);return t<100&&0<=t&&isFinite(e.getFullYear())&&e.setFullYear(t),e}function Ut(t){var e=new Date(Date.UTC.apply(null,arguments));return t<100&&0<=t&&isFinite(e.getUTCFullYear())&&e.setUTCFullYear(t),e}function Gt(t,e,i){i=7+e-i;return i-(7+Ut(t,0,i).getUTCDay()-e)%7-1}function Vt(t,e,i,n,o){var s,e=1+7*(e-1)+(7+i-n)%7+Gt(t,n,o),i=e<=0?Yt(s=t-1)+e:e>Yt(t)?(s=t+1,e-Yt(t)):(s=t,e);return{year:s,dayOfYear:i}}function Xt(t,e,i){var n,o,s=Gt(t.year(),e,i),s=Math.floor((t.dayOfYear()-s-1)/7)+1;return s<1?n=s+Zt(o=t.year()-1,e,i):s>Zt(t.year(),e,i)?(n=s-Zt(t.year(),e,i),o=t.year()+1):(o=t.year(),n=s),{week:n,year:o}}function Zt(t,e,i){var n=Gt(t,e,i),e=Gt(t+1,e,i);return(Yt(t)-n+e)/7}n("w",["ww",2],"wo","week"),n("W",["WW",2],"Wo","isoWeek"),e("week","w"),e("isoWeek","W"),i("week",5),i("isoWeek",5),_("w",b),_("ww",b,o),_("W",b),_("WW",b,o),At(["w","ww","W","WW"],function(t,e,i,n){e[n.substr(0,1)]=d(t)});n("d",0,"do","day"),n("dd",0,0,function(t){return this.localeData().weekdaysMin(this,t)}),n("ddd",0,0,function(t){return this.localeData().weekdaysShort(this,t)}),n("dddd",0,0,function(t){return this.localeData().weekdays(this,t)}),n("e",0,0,"weekday"),n("E",0,0,"isoWeekday"),e("day","d"),e("weekday","e"),e("isoWeekday","E"),i("day",11),i("weekday",11),i("isoWeekday",11),_("d",b),_("e",b),_("E",b),_("dd",function(t,e){return e.weekdaysMinRegex(t)}),_("ddd",function(t,e){return e.weekdaysShortRegex(t)}),_("dddd",function(t,e){return e.weekdaysRegex(t)}),At(["dd","ddd","dddd"],function(t,e,i,n){n=i._locale.weekdaysParse(t,n,i._strict);null!=n?e.d=n:g(i).invalidWeekday=t}),At(["d","e","E"],function(t,e,i,n){e[n]=d(t)});var Qt="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_");var Jt="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_");var Kt="Su_Mo_Tu_We_Th_Fr_Sa".split("_");var te=Tt;var ee=Tt;var ie=Tt;function ne(){function t(t,e){return e.length-t.length}for(var e,i,n,o=[],s=[],r=[],a=[],l=0;l<7;l++)n=c([2e3,1]).day(l),e=this.weekdaysMin(n,""),i=this.weekdaysShort(n,""),n=this.weekdays(n,""),o.push(e),s.push(i),r.push(n),a.push(e),a.push(i),a.push(n);for(o.sort(t),s.sort(t),r.sort(t),a.sort(t),l=0;l<7;l++)s[l]=Et(s[l]),r[l]=Et(r[l]),a[l]=Et(a[l]);this._weekdaysRegex=new RegExp("^("+a.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+r.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+s.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+o.join("|")+")","i")}function oe(){return this.hours()%12||12}function se(t,e){n(t,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),e)})}function re(t,e){return e._meridiemParse}n("H",["HH",2],0,"hour"),n("h",["hh",2],0,oe),n("k",["kk",2],0,function(){return this.hours()||24}),n("hmm",0,0,function(){return""+oe.apply(this)+l(this.minutes(),2)}),n("hmmss",0,0,function(){return""+oe.apply(this)+l(this.minutes(),2)+l(this.seconds(),2)}),n("Hmm",0,0,function(){return""+this.hours()+l(this.minutes(),2)}),n("Hmmss",0,0,function(){return""+this.hours()+l(this.minutes(),2)+l(this.seconds(),2)}),se("a",!0),se("A",!1),e("hour","h"),i("hour",13),_("a",re),_("A",re),_("H",b),_("h",b),_("k",b),_("HH",b,o),_("hh",b,o),_("kk",b,o),_("hmm",gt),_("hmmss",yt),_("Hmm",gt),_("Hmmss",yt),k(["H","HH"],C),k(["k","kk"],function(t,e,i){t=d(t);e[C]=24===t?0:t}),k(["a","A"],function(t,e,i){i._isPm=i._locale.isPM(t),i._meridiem=t}),k(["h","hh"],function(t,e,i){e[C]=d(t),g(i).bigHour=!0}),k("hmm",function(t,e,i){var n=t.length-2;e[C]=d(t.substr(0,n)),e[E]=d(t.substr(n)),g(i).bigHour=!0}),k("hmmss",function(t,e,i){var n=t.length-4,o=t.length-2;e[C]=d(t.substr(0,n)),e[E]=d(t.substr(n,2)),e[D]=d(t.substr(o)),g(i).bigHour=!0}),k("Hmm",function(t,e,i){var n=t.length-2;e[C]=d(t.substr(0,n)),e[E]=d(t.substr(n))}),k("Hmmss",function(t,e,i){var n=t.length-4,o=t.length-2;e[C]=d(t.substr(0,n)),e[E]=d(t.substr(n,2)),e[D]=d(t.substr(o))});var ae,Tt=ot("Hours",!0),le={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Mt,monthsShort:Nt,week:{dow:0,doy:6},weekdays:Qt,weekdaysMin:Kt,weekdaysShort:Jt,meridiemParse:/[ap]\.?m?\.?/i},L={},ce={};function de(t){return t&&t.toLowerCase().replace("_","-")}function ue(t){var e;if(!L[t]&&"undefined"!=typeof module&&module&&module.exports)try{e=ae._abbr,require("./locale/"+t),he(e)}catch(t){}return L[t]}function he(t,e){return(ae=t&&(t=h(e)?fe(t):pe(t,e))?t:ae)._abbr}function pe(t,e){if(null===e)return delete L[t],null;var i=le;if(e.abbr=t,null!=L[t])Q("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),i=L[t]._config;else if(null!=e.parentLocale){if(null==L[e.parentLocale])return ce[e.parentLocale]||(ce[e.parentLocale]=[]),ce[e.parentLocale].push({name:t,config:e}),null;i=L[e.parentLocale]._config}return L[t]=new K(J(i,e)),ce[t]&&ce[t].forEach(function(t){pe(t.name,t.config)}),he(t),L[t]}function fe(t){var e;if(!(t=t&&t._locale&&t._locale._abbr?t._locale._abbr:t))return ae;if(!u(t)){if(e=ue(t))return e;t=[t]}for(var i,n,o,s,r=t,a=0;a<r.length;){for(i=(s=de(r[a]).split("-")).length,n=(n=de(r[a+1]))?n.split("-"):null;0<i;){if(o=ue(s.slice(0,i).join("-")))return o;if(n&&n.length>=i&&V(s,n,!0)>=i-1)break;i--}a++}return null}function me(t){var e=t._a;return e&&-2===g(t).overflow&&(e=e[T]<0||11<e[T]?T:e[S]<1||e[S]>jt(e[x],e[T])?S:e[C]<0||24<e[C]||24===e[C]&&(0!==e[E]||0!==e[D]||0!==e[Lt])?C:e[E]<0||59<e[E]?E:e[D]<0||59<e[D]?D:e[Lt]<0||999<e[Lt]?Lt:-1,g(t)._overflowDayOfYear&&(e<x||S<e)&&(e=S),g(t)._overflowWeeks&&-1===e&&(e=Ot),g(t)._overflowWeekday&&-1===e&&(e=$t),g(t).overflow=e),t}var ge=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,ye=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,ve=/Z|[+-]\d\d(?::?\d\d)?/,be=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],we=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],_e=/^\/?Date\((\-?\d+)/i;function ke(t){var e,i,n,o,s,r,a=t._i,l=ge.exec(a)||ye.exec(a);if(l){for(g(t).iso=!0,e=0,i=be.length;e<i;e++)if(be[e][1].exec(l[1])){o=be[e][0],n=!1!==be[e][2];break}if(null==o)t._isValid=!1;else{if(l[3]){for(e=0,i=we.length;e<i;e++)if(we[e][1].exec(l[3])){s=(l[2]||" ")+we[e][0];break}if(null==s)return void(t._isValid=!1)}if(n||null==s){if(l[4]){if(!ve.exec(l[4]))return void(t._isValid=!1);r="Z"}t._f=o+(s||"")+(r||""),Ee(t)}else t._isValid=!1}}else t._isValid=!1}var xe=/^((?:Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d?\d\s(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(?:\d\d)?\d\d\s)(\d\d:\d\d)(\:\d\d)?(\s(?:UT|GMT|[ECMP][SD]T|[A-IK-Za-ik-z]|[+-]\d{4}))$/;function Te(t){var e,i,n,o,s={" GMT":" +0000"," EDT":" -0400"," EST":" -0500"," CDT":" -0500"," CST":" -0600"," MDT":" -0600"," MST":" -0700"," PDT":" -0700"," PST":" -0800"},r=t._i.replace(/\([^\)]*\)|[\n\t]/g," ").replace(/(\s\s+)/g," ").replace(/^\s|\s$/g,""),a=xe.exec(r);if(a){if(r=a[1]?"ddd"+(5===a[1].length?", ":" "):"",e="D MMM "+(10<a[2].length?"YYYY ":"YY "),i="HH:mm"+(a[4]?":ss":""),a[1]){var l=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][new Date(a[2]).getDay()];if(a[1].substr(0,3)!==l)return g(t).weekdayMismatch=!0,void(t._isValid=!1)}switch(a[5].length){case 2:n=0===o?" +0000":((o="YXWVUTSRQPONZABCDEFGHIKLM".indexOf(a[5][1].toUpperCase())-12)<0?" -":" +")+(""+o).replace(/^-?/,"0").match(/..$/)[0]+"00";break;case 4:n=s[a[5]];break;default:n=s[" GMT"]}a[5]=n,t._i=a.splice(1).join(""),t._f=r+e+i+" ZZ",Ee(t),g(t).rfc2822=!0}else t._isValid=!1}function Se(t,e,i){return null!=t?t:null!=e?e:i}function Ce(t){var e,i,n,o,s,r,a,l,c,d,u,h=[];if(!t._d){for(n=t,o=new Date(p.now()),i=n._useUTC?[o.getUTCFullYear(),o.getUTCMonth(),o.getUTCDate()]:[o.getFullYear(),o.getMonth(),o.getDate()],t._w&&null==t._a[S]&&null==t._a[T]&&(null!=(o=(n=t)._w).GG||null!=o.W||null!=o.E?(l=1,c=4,s=Se(o.GG,n._a[x],Xt(O(),1,4).year),r=Se(o.W,1),((a=Se(o.E,1))<1||7<a)&&(d=!0)):(l=n._locale._week.dow,c=n._locale._week.doy,u=Xt(O(),l,c),s=Se(o.gg,n._a[x],u.year),r=Se(o.w,u.week),null!=o.d?((a=o.d)<0||6<a)&&(d=!0):null!=o.e?(a=o.e+l,(o.e<0||6<o.e)&&(d=!0)):a=l),r<1||r>Zt(s,l,c)?g(n)._overflowWeeks=!0:null!=d?g(n)._overflowWeekday=!0:(u=Vt(s,r,a,l,c),n._a[x]=u.year,n._dayOfYear=u.dayOfYear)),null!=t._dayOfYear&&(o=Se(t._a[x],i[x]),(t._dayOfYear>Yt(o)||0===t._dayOfYear)&&(g(t)._overflowDayOfYear=!0),d=Ut(o,0,t._dayOfYear),t._a[T]=d.getUTCMonth(),t._a[S]=d.getUTCDate()),e=0;e<3&&null==t._a[e];++e)t._a[e]=h[e]=i[e];for(;e<7;e++)t._a[e]=h[e]=null==t._a[e]?2===e?1:0:t._a[e];24===t._a[C]&&0===t._a[E]&&0===t._a[D]&&0===t._a[Lt]&&(t._nextDay=!0,t._a[C]=0),t._d=(t._useUTC?Ut:zt).apply(null,h),null!=t._tzm&&t._d.setUTCMinutes(t._d.getUTCMinutes()-t._tzm),t._nextDay&&(t._a[C]=24)}}function Ee(t){if(t._f===p.ISO_8601)ke(t);else if(t._f===p.RFC_2822)Te(t);else{t._a=[],g(t).empty=!0;for(var e,i,n,o,s,r=""+t._i,a=r.length,l=0,c=ht(t._f,t._locale).match(at)||[],d=0;d<c.length;d++)i=c[d],(e=(r.match(Ct(i,t))||[])[0])&&(0<(n=r.substr(0,r.indexOf(e))).length&&g(t).unusedInput.push(n),r=r.slice(r.indexOf(e)+e.length),l+=e.length),dt[i]?(e?g(t).empty=!1:g(t).unusedTokens.push(i),n=i,s=t,null!=(o=e)&&f(Dt,n)&&Dt[n](o,s._a,s,n)):t._strict&&!e&&g(t).unusedTokens.push(i);g(t).charsLeftOver=a-l,0<r.length&&g(t).unusedInput.push(r),t._a[C]<=12&&!0===g(t).bigHour&&0<t._a[C]&&(g(t).bigHour=void 0),g(t).parsedDateParts=t._a.slice(0),g(t).meridiem=t._meridiem,t._a[C]=function(t,e,i){if(null==i)return e;return null!=t.meridiemHour?t.meridiemHour(e,i):null!=t.isPM?((t=t.isPM(i))&&e<12&&(e+=12),e=t||12!==e?e:0):e}(t._locale,t._a[C],t._meridiem),Ce(t),me(t)}}function De(t){var e,i,n=t._i,o=t._f;if(t._locale=t._locale||fe(t._l),null===n||void 0===o&&""===n)return W({nullInput:!0});if("string"==typeof n&&(t._i=n=t._locale.preparse(n)),y(n))return new G(me(n));if(F(n))t._d=n;else if(u(o)){var s,r,a,l,c,d=t;if(0===d._f.length)g(d).invalidFormat=!0,d._d=new Date(NaN);else{for(l=0;l<d._f.length;l++)c=0,s=z({},d),null!=d._useUTC&&(s._useUTC=d._useUTC),s._f=d._f[l],Ee(s),Y(s)&&(c=(c+=g(s).charsLeftOver)+10*g(s).unusedTokens.length,g(s).score=c,null==a||c<a)&&(a=c,r=s);m(d,r||s)}}else if(o)Ee(t);else if(h(o=(n=t)._i))n._d=new Date(p.now());else F(o)?n._d=new Date(o.valueOf()):"string"==typeof o?(i=n,null!==(e=_e.exec(i._i))?i._d=new Date(+e[1]):(ke(i),!1===i._isValid&&(delete i._isValid,Te(i),!1===i._isValid)&&(delete i._isValid,p.createFromInputFallback(i)))):u(o)?(n._a=R(o.slice(0),function(t){return parseInt(t,10)}),Ce(n)):I(o)?(e=n)._d||(i=it(e._i),e._a=R([i.year,i.month,i.day||i.date,i.hour,i.minute,i.second,i.millisecond],function(t){return t&&parseInt(t,10)}),Ce(e)):H(o)?n._d=new Date(o):p.createFromInputFallback(n);return Y(t)||(t._d=null),t}function Ae(t,e,i,n,o){var s={};return!0!==i&&!1!==i||(n=i,i=void 0),(I(t)&&function(t){for(var e in t)return;return 1}(t)||u(t)&&0===t.length)&&(t=void 0),s._isAMomentObject=!0,s._useUTC=s._isUTC=o,s._l=i,s._i=t,s._f=e,s._strict=n,(o=new G(me(De(o=s))))._nextDay&&(o.add(1,"d"),o._nextDay=void 0),o}function O(t,e,i,n){return Ae(t,e,i,n,!1)}p.createFromInputFallback=t("value provided is not in a recognized RFC2822 or ISO format. moment construction falls back to js Date(), which is not reliable across all browsers and versions. Non RFC2822/ISO date formats are discouraged and will be removed in an upcoming major release. Please refer to http://momentjs.com/guides/#/warnings/js-date/ for more info.",function(t){t._d=new Date(t._i+(t._useUTC?" UTC":""))}),p.ISO_8601=function(){},p.RFC_2822=function(){};gt=t("moment().min is deprecated, use moment.max instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var t=O.apply(null,arguments);return this.isValid()&&t.isValid()?t<this?this:t:W()}),yt=t("moment().max is deprecated, use moment.min instead. http://momentjs.com/guides/#/warnings/min-max/",function(){var t=O.apply(null,arguments);return this.isValid()&&t.isValid()?this<t?this:t:W()});function Le(t,e){var i,n;if(!(e=1===e.length&&u(e[0])?e[0]:e).length)return O();for(i=e[0],n=1;n<e.length;++n)e[n].isValid()&&!e[n][t](i)||(i=e[n]);return i}var Oe=["year","quarter","month","week","day","hour","minute","second","millisecond"];function $e(t){var t=it(t),e=t.year||0,i=t.quarter||0,n=t.month||0,o=t.week||0,s=t.day||0,r=t.hour||0,a=t.minute||0,l=t.second||0,c=t.millisecond||0;this._isValid=function(t){for(var e in t)if(-1===Oe.indexOf(e)||null!=t[e]&&isNaN(t[e]))return!1;for(var i=!1,n=0;n<Oe.length;++n)if(t[Oe[n]]){if(i)return!1;parseFloat(t[Oe[n]])!==d(t[Oe[n]])&&(i=!0)}return!0}(t),this._milliseconds=+c+1e3*l+6e4*a+1e3*r*60*60,this._days=+s+7*o,this._months=+n+3*i+12*e,this._data={},this._locale=fe(),this._bubble()}function Me(t){return t instanceof $e}function je(t){return t<0?-1*Math.round(-1*t):Math.round(t)}function Pe(t,i){n(t,0,0,function(){var t=this.utcOffset(),e="+";return t<0&&(t=-t,e="-"),e+l(~~(t/60),2)+i+l(~~t%60,2)})}Pe("Z",":"),Pe("ZZ",""),_("Z",xt),_("ZZ",xt),k(["Z","ZZ"],function(t,e,i){i._useUTC=!0,i._tzm=Ie(xt,t)});var Ne=/([\+\-]|\d\d)/gi;function Ie(t,e){var e=(e||"").match(t);return null===e?null:0===(e=60*(t=((e[e.length-1]||[])+"").match(Ne)||["-",0,0])[1]+d(t[2]))?0:"+"===t[0]?e:-e}function He(t,e){var i;return e._isUTC?(e=e.clone(),i=(y(t)||F(t)?t:O(t)).valueOf()-e.valueOf(),e._d.setTime(e._d.valueOf()+i),p.updateOffset(e,!1),e):O(t).local()}function Fe(t){return 15*-Math.round(t._d.getTimezoneOffset()/15)}function Re(){return!!this.isValid()&&this._isUTC&&0===this._offset}p.updateOffset=function(){};var qe=/^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)(\.\d*)?)?$/,Ye=/^(-)?P(?:(-?[0-9,.]*)Y)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)W)?(?:(-?[0-9,.]*)D)?(?:T(?:(-?[0-9,.]*)H)?(?:(-?[0-9,.]*)M)?(?:(-?[0-9,.]*)S)?)?$/;function $(t,e){var i,n=t;return Me(t)?n={ms:t._milliseconds,d:t._days,M:t._months}:H(t)?(n={},e?n[e]=t:n.milliseconds=t):(e=qe.exec(t))?(i="-"===e[1]?-1:1,n={y:0,d:d(e[S])*i,h:d(e[C])*i,m:d(e[E])*i,s:d(e[D])*i,ms:d(je(1e3*e[Lt]))*i}):(e=Ye.exec(t))?(i="-"===e[1]?-1:1,n={y:We(e[2],i),M:We(e[3],i),w:We(e[4],i),d:We(e[5],i),h:We(e[6],i),m:We(e[7],i),s:We(e[8],i)}):null==n?n={}:"object"==typeof n&&("from"in n||"to"in n)&&(e=function(t,e){var i;if(!t.isValid()||!e.isValid())return{milliseconds:0,months:0};e=He(e,t),t.isBefore(e)?i=Be(t,e):((i=Be(e,t)).milliseconds=-i.milliseconds,i.months=-i.months);return i}(O(n.from),O(n.to)),(n={}).ms=e.milliseconds,n.M=e.months),i=new $e(n),Me(t)&&f(t,"_locale")&&(i._locale=t._locale),i}function We(t,e){t=t&&parseFloat(t.replace(",","."));return(isNaN(t)?0:t)*e}function Be(t,e){var i={milliseconds:0,months:0};return i.months=e.month()-t.month()+12*(e.year()-t.year()),t.clone().add(i.months,"M").isAfter(e)&&--i.months,i.milliseconds=+e-+t.clone().add(i.months,"M"),i}function ze(n,o){return function(t,e){var i;return null===e||isNaN(+e)||(Q(o,"moment()."+o+"(period, number) is deprecated. Please use moment()."+o+"(number, period). See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info."),i=t,t=e,e=i),Ue(this,$(t="string"==typeof t?+t:t,e),n),this}}function Ue(t,e,i,n){var o=e._milliseconds,s=je(e._days),e=je(e._months);t.isValid()&&(n=null==n||n,o&&t._d.setTime(t._d.valueOf()+o*i),s&&rt(t,"Date",st(t,"Date")+s*i),e&&It(t,st(t,"Month")+e*i),n)&&p.updateOffset(t,s||e)}$.fn=$e.prototype,$.invalid=function(){return $(NaN)};Mt=ze(1,"add"),Nt=ze(-1,"subtract");function Ge(t){return void 0===t?this._locale._abbr:(null!=(t=fe(t))&&(this._locale=t),this)}p.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",p.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";Qt=t("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",function(t){return void 0===t?this.localeData():this.locale(t)});function Ve(){return this._locale}function Xe(t,e){n(0,[t,t.length],0,e)}function Ze(t,e,i,n,o){var s;return null==t?Xt(this,n,o).year:(s=Zt(t,n,o),function(t,e,i,n,o){t=Vt(t,e,i,n,o),e=Ut(t.year,0,t.dayOfYear);return this.year(e.getUTCFullYear()),this.month(e.getUTCMonth()),this.date(e.getUTCDate()),this}.call(this,t,e=s<e?s:e,i,n,o))}n(0,["gg",2],0,function(){return this.weekYear()%100}),n(0,["GG",2],0,function(){return this.isoWeekYear()%100}),Xe("gggg","weekYear"),Xe("ggggg","weekYear"),Xe("GGGG","isoWeekYear"),Xe("GGGGG","isoWeekYear"),e("weekYear","gg"),e("isoWeekYear","GG"),i("weekYear",1),i("isoWeekYear",1),_("G",_t),_("g",_t),_("GG",b,o),_("gg",b,o),_("GGGG",bt,mt),_("gggg",bt,mt),_("GGGGG",w,v),_("ggggg",w,v),At(["gggg","ggggg","GGGG","GGGGG"],function(t,e,i,n){e[n.substr(0,2)]=d(t)}),At(["gg","GG"],function(t,e,i,n){e[n]=p.parseTwoDigitYear(t)}),n("Q",0,"Qo","quarter"),e("quarter","Q"),i("quarter",7),_("Q",pt),k("Q",function(t,e){e[T]=3*(d(t)-1)}),n("D",["DD",2],"Do","date"),e("date","D"),i("date",9),_("D",b),_("DD",b,o),_("Do",function(t,e){return t?e._dayOfMonthOrdinalParse||e._ordinalParse:e._dayOfMonthOrdinalParseLenient}),k(["D","DD"],S),k("Do",function(t,e){e[S]=d(t.match(b)[0])});Kt=ot("Date",!0);n("DDD",["DDDD",3],"DDDo","dayOfYear"),e("dayOfYear","DDD"),i("dayOfYear",4),_("DDD",vt),_("DDDD",ft),k(["DDD","DDDD"],function(t,e,i){i._dayOfYear=d(t)}),n("m",["mm",2],0,"minute"),e("minute","m"),i("minute",14),_("m",b),_("mm",b,o),k(["m","mm"],E);var Qe,Jt=ot("Minutes",!1),bt=(n("s",["ss",2],0,"second"),e("second","s"),i("second",15),_("s",b),_("ss",b,o),k(["s","ss"],D),ot("Seconds",!1));for(n("S",0,0,function(){return~~(this.millisecond()/100)}),n(0,["SS",2],0,function(){return~~(this.millisecond()/10)}),n(0,["SSS",3],0,"millisecond"),n(0,["SSSS",4],0,function(){return 10*this.millisecond()}),n(0,["SSSSS",5],0,function(){return 100*this.millisecond()}),n(0,["SSSSSS",6],0,function(){return 1e3*this.millisecond()}),n(0,["SSSSSSS",7],0,function(){return 1e4*this.millisecond()}),n(0,["SSSSSSSS",8],0,function(){return 1e5*this.millisecond()}),n(0,["SSSSSSSSS",9],0,function(){return 1e6*this.millisecond()}),e("millisecond","ms"),i("millisecond",16),_("S",vt,pt),_("SS",vt,o),_("SSS",vt,ft),Qe="SSSS";Qe.length<=9;Qe+="S")_(Qe,wt);function Je(t,e){e[Lt]=d(1e3*("0."+t))}for(Qe="S";Qe.length<=9;Qe+="S")k(Qe,Je);mt=ot("Milliseconds",!1);n("z",0,0,"zoneAbbr"),n("zz",0,0,"zoneName");w=G.prototype;function Ke(t){return t}w.add=Mt,w.calendar=function(t,e){var i=He(t=t||O(),this).startOf("day"),i=p.calendarFormat(this,i)||"sameElse",e=e&&(r(e[i])?e[i].call(this,t):e[i]);return this.format(e||this.localeData().calendar(i,this,O(t)))},w.clone=function(){return new G(this)},w.diff=function(t,e,i){var n,o;return this.isValid()&&(t=He(t,this)).isValid()?(n=6e4*(t.utcOffset()-this.utcOffset()),"year"===(e=s(e))||"month"===e||"quarter"===e?(o=function(t,e){var i,n=12*(e.year()-t.year())+(e.month()-t.month()),o=t.clone().add(n,"months");t=e-o<0?(i=t.clone().add(n-1,"months"),(e-o)/(o-i)):(i=t.clone().add(1+n,"months"),(e-o)/(i-o));return-(n+t)||0}(this,t),"quarter"===e?o/=3:"year"===e&&(o/=12)):(t=this-t,o="second"===e?t/1e3:"minute"===e?t/6e4:"hour"===e?t/36e5:"day"===e?(t-n)/864e5:"week"===e?(t-n)/6048e5:t),i?o:a(o)):NaN},w.endOf=function(t){return void 0===(t=s(t))||"millisecond"===t?this:this.startOf(t="date"===t?"day":t).add(1,"isoWeek"===t?"week":t).subtract(1,"ms")},w.format=function(t){return t=t||(this.isUtc()?p.defaultFormatUtc:p.defaultFormat),t=ut(this,t),this.localeData().postformat(t)},w.from=function(t,e){return this.isValid()&&(y(t)&&t.isValid()||O(t).isValid())?$({to:this,from:t}).locale(this.locale()).humanize(!e):this.localeData().invalidDate()},w.fromNow=function(t){return this.from(O(),t)},w.to=function(t,e){return this.isValid()&&(y(t)&&t.isValid()||O(t).isValid())?$({from:this,to:t}).locale(this.locale()).humanize(!e):this.localeData().invalidDate()},w.toNow=function(t){return this.to(O(),t)},w.get=function(t){return r(this[t=s(t)])?this[t]():this},w.invalidAt=function(){return g(this).overflow},w.isAfter=function(t,e){return t=y(t)?t:O(t),!(!this.isValid()||!t.isValid())&&("millisecond"===(e=s(h(e)?"millisecond":e))?this.valueOf()>t.valueOf():t.valueOf()<this.clone().startOf(e).valueOf())},w.isBefore=function(t,e){return t=y(t)?t:O(t),!(!this.isValid()||!t.isValid())&&("millisecond"===(e=s(h(e)?"millisecond":e))?this.valueOf()<t.valueOf():this.clone().endOf(e).valueOf()<t.valueOf())},w.isBetween=function(t,e,i,n){return("("===(n=n||"()")[0]?this.isAfter(t,i):!this.isBefore(t,i))&&(")"===n[1]?this.isBefore(e,i):!this.isAfter(e,i))},w.isSame=function(t,e){var t=y(t)?t:O(t);return!(!this.isValid()||!t.isValid())&&("millisecond"===(e=s(e||"millisecond"))?this.valueOf()===t.valueOf():(t=t.valueOf(),this.clone().startOf(e).valueOf()<=t&&t<=this.clone().endOf(e).valueOf()))},w.isSameOrAfter=function(t,e){return this.isSame(t,e)||this.isAfter(t,e)},w.isSameOrBefore=function(t,e){return this.isSame(t,e)||this.isBefore(t,e)},w.isValid=function(){return Y(this)},w.lang=Qt,w.locale=Ge,w.localeData=Ve,w.max=yt,w.min=gt,w.parsingFlags=function(){return m({},g(this))},w.set=function(t,e){if("object"==typeof t)for(var i=function(t){var e,i=[];for(e in t)i.push({unit:e,priority:nt[e]});return i.sort(function(t,e){return t.priority-e.priority}),i}(t=it(t)),n=0;n<i.length;n++)this[i[n].unit](t[i[n].unit]);else if(r(this[t=s(t)]))return this[t](e);return this},w.startOf=function(t){switch(t=s(t)){case"year":this.month(0);case"quarter":case"month":this.date(1);case"week":case"isoWeek":case"day":case"date":this.hours(0);case"hour":this.minutes(0);case"minute":this.seconds(0);case"second":this.milliseconds(0)}return"week"===t&&this.weekday(0),"isoWeek"===t&&this.isoWeekday(1),"quarter"===t&&this.month(3*Math.floor(this.month()/3)),this},w.subtract=Nt,w.toArray=function(){return[this.year(),this.month(),this.date(),this.hour(),this.minute(),this.second(),this.millisecond()]},w.toObject=function(){return{years:this.year(),months:this.month(),date:this.date(),hours:this.hours(),minutes:this.minutes(),seconds:this.seconds(),milliseconds:this.milliseconds()}},w.toDate=function(){return new Date(this.valueOf())},w.toISOString=function(){var t;return this.isValid()?(t=this.clone().utc()).year()<0||9999<t.year()?ut(t,"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]"):r(Date.prototype.toISOString)?this.toDate().toISOString():ut(t,"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]"):null},w.inspect=function(){var t,e,i;return this.isValid()?(e="moment",t="",this.isLocal()||(e=0===this.utcOffset()?"moment.utc":"moment.parseZone",t="Z"),e="["+e+'("]',i=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY",this.format(e+i+"-MM-DD[T]HH:mm:ss.SSS"+(t+'[")]'))):"moment.invalid(/* "+this._i+" */)"},w.toJSON=function(){return this.isValid()?this.toISOString():null},w.toString=function(){return this.clone().locale("en").format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ")},w.unix=function(){return Math.floor(this.valueOf()/1e3)},w.valueOf=function(){return this._d.valueOf()-6e4*(this._offset||0)},w.creationData=function(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}},w.year=Bt,w.isLeapYear=function(){return Wt(this.year())},w.weekYear=function(t){return Ze.call(this,t,this.week(),this.weekday(),this.localeData()._week.dow,this.localeData()._week.doy)},w.isoWeekYear=function(t){return Ze.call(this,t,this.isoWeek(),this.isoWeekday(),1,4)},w.quarter=w.quarters=function(t){return null==t?Math.ceil((this.month()+1)/3):this.month(3*(t-1)+this.month()%3)},w.month=Ht,w.daysInMonth=function(){return jt(this.year(),this.month())},w.week=w.weeks=function(t){var e=this.localeData().week(this);return null==t?e:this.add(7*(t-e),"d")},w.isoWeek=w.isoWeeks=function(t){var e=Xt(this,1,4).week;return null==t?e:this.add(7*(t-e),"d")},w.weeksInYear=function(){var t=this.localeData()._week;return Zt(this.year(),t.dow,t.doy)},w.isoWeeksInYear=function(){return Zt(this.year(),1,4)},w.date=Kt,w.day=w.days=function(t){var e,i,n;return this.isValid()?(e=this._isUTC?this._d.getUTCDay():this._d.getDay(),null!=t?(i=t,n=this.localeData(),t="string"!=typeof i?i:isNaN(i)?"number"==typeof(i=n.weekdaysParse(i))?i:null:parseInt(i,10),this.add(t-e,"d")):e):null!=t?this:NaN},w.weekday=function(t){var e;return this.isValid()?(e=(this.day()+7-this.localeData()._week.dow)%7,null==t?e:this.add(t-e,"d")):null!=t?this:NaN},w.isoWeekday=function(t){var e,i;return this.isValid()?null!=t?(e=t,i=this.localeData(),i="string"==typeof e?i.weekdaysParse(e)%7||7:isNaN(e)?null:e,this.day(this.day()%7?i:i-7)):this.day()||7:null!=t?this:NaN},w.dayOfYear=function(t){var e=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==t?e:this.add(t-e,"d")},w.hour=w.hours=Tt,w.minute=w.minutes=Jt,w.second=w.seconds=bt,w.millisecond=w.milliseconds=mt,w.utcOffset=function(t,e,i){var n,o=this._offset||0;if(!this.isValid())return null!=t?this:NaN;if(null==t)return this._isUTC?o:Fe(this);if("string"==typeof t){if(null===(t=Ie(xt,t)))return this}else Math.abs(t)<16&&!i&&(t*=60);return!this._isUTC&&e&&(n=Fe(this)),this._offset=t,this._isUTC=!0,null!=n&&this.add(n,"m"),o!==t&&(!e||this._changeInProgress?Ue(this,$(t-o,"m"),1,!1):this._changeInProgress||(this._changeInProgress=!0,p.updateOffset(this,!0),this._changeInProgress=null)),this},w.utc=function(t){return this.utcOffset(0,t)},w.local=function(t){return this._isUTC&&(this.utcOffset(0,t),this._isUTC=!1,t)&&this.subtract(Fe(this),"m"),this},w.parseZone=function(){var t;return null!=this._tzm?this.utcOffset(this._tzm,!1,!0):"string"==typeof this._i&&(null!=(t=Ie(kt,this._i))?this.utcOffset(t):this.utcOffset(0,!0)),this},w.hasAlignedHourOffset=function(t){return!!this.isValid()&&(t=t?O(t).utcOffset():0,(this.utcOffset()-t)%60==0)},w.isDST=function(){return this.utcOffset()>this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},w.isLocal=function(){return!!this.isValid()&&!this._isUTC},w.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},w.isUtc=Re,w.isUTC=Re,w.zoneAbbr=function(){return this._isUTC?"UTC":""},w.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},w.dates=t("dates accessor is deprecated. Use date instead.",Kt),w.months=t("months accessor is deprecated. Use month instead",Ht),w.years=t("years accessor is deprecated. Use year instead",Bt),w.zone=t("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(t,e){return null!=t?(this.utcOffset(t="string"!=typeof t?-t:t,e),this):-this.utcOffset()}),w.isDSTShifted=t("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){var t,e;return h(this._isDSTShifted)&&(z(t={},this),(t=De(t))._a?(e=(t._isUTC?c:O)(t._a),this._isDSTShifted=this.isValid()&&0<V(t._a,e.toArray())):this._isDSTShifted=!1),this._isDSTShifted});v=K.prototype;function ti(t,e,i,n){var o=fe(),n=c().set(n,e);return o[i](n,t)}function ei(t,e,i){if(H(t)&&(e=t,t=void 0),t=t||"",null!=e)return ti(t,e,i,"month");for(var n=[],o=0;o<12;o++)n[o]=ti(t,o,i,"month");return n}function ii(t,e,i,n){e=("boolean"==typeof t?H(e)&&(i=e,e=void 0):(e=t,t=!1,H(i=e)&&(i=e,e=void 0)),e||"");var o=fe(),s=t?o._week.dow:0;if(null!=i)return ti(e,(i+s)%7,n,"day");for(var r=[],a=0;a<7;a++)r[a]=ti(e,(a+s)%7,n,"day");return r}v.calendar=function(t,e,i){return r(t=this._calendar[t]||this._calendar.sameElse)?t.call(e,i):t},v.longDateFormat=function(t){var e=this._longDateFormat[t],i=this._longDateFormat[t.toUpperCase()];return e||!i?e:(this._longDateFormat[t]=i.replace(/MMMM|MM|DD|dddd/g,function(t){return t.slice(1)}),this._longDateFormat[t])},v.invalidDate=function(){return this._invalidDate},v.ordinal=function(t){return this._ordinal.replace("%d",t)},v.preparse=Ke,v.postformat=Ke,v.relativeTime=function(t,e,i,n){var o=this._relativeTime[i];return r(o)?o(t,e,i,n):o.replace(/%d/i,t)},v.pastFuture=function(t,e){return r(t=this._relativeTime[0<t?"future":"past"])?t(e):t.replace(/%s/i,e)},v.set=function(t){var e,i;for(i in t)r(e=t[i])?this[i]=e:this["_"+i]=e;this._config=t,this._dayOfMonthOrdinalParseLenient=new RegExp((this._dayOfMonthOrdinalParse.source||this._ordinalParse.source)+"|"+/\d{1,2}/.source)},v.months=function(t,e){return t?(u(this._months)?this._months:this._months[(this._months.isFormat||Pt).test(e)?"format":"standalone"])[t.month()]:u(this._months)?this._months:this._months.standalone},v.monthsShort=function(t,e){return t?(u(this._monthsShort)?this._monthsShort:this._monthsShort[Pt.test(e)?"format":"standalone"])[t.month()]:u(this._monthsShort)?this._monthsShort:this._monthsShort.standalone},v.monthsParse=function(t,e,i){var n,o;if(this._monthsParseExact)return function(t,e,i){var n,o,s,t=t.toLocaleLowerCase();if(!this._monthsParse)for(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[],n=0;n<12;++n)s=c([2e3,n]),this._shortMonthsParse[n]=this.monthsShort(s,"").toLocaleLowerCase(),this._longMonthsParse[n]=this.months(s,"").toLocaleLowerCase();return i?"MMM"===e?-1!==(o=A.call(this._shortMonthsParse,t))?o:null:-1!==(o=A.call(this._longMonthsParse,t))?o:null:"MMM"===e?-1!==(o=A.call(this._shortMonthsParse,t))||-1!==(o=A.call(this._longMonthsParse,t))?o:null:-1!==(o=A.call(this._longMonthsParse,t))||-1!==(o=A.call(this._shortMonthsParse,t))?o:null}.call(this,t,e,i);for(this._monthsParse||(this._monthsParse=[],this._longMonthsParse=[],this._shortMonthsParse=[]),n=0;n<12;n++){if(o=c([2e3,n]),i&&!this._longMonthsParse[n]&&(this._longMonthsParse[n]=new RegExp("^"+this.months(o,"").replace(".","")+"$","i"),this._shortMonthsParse[n]=new RegExp("^"+this.monthsShort(o,"").replace(".","")+"$","i")),i||this._monthsParse[n]||(o="^"+this.months(o,"")+"|^"+this.monthsShort(o,""),this._monthsParse[n]=new RegExp(o.replace(".",""),"i")),i&&"MMMM"===e&&this._longMonthsParse[n].test(t))return n;if(i&&"MMM"===e&&this._shortMonthsParse[n].test(t))return n;if(!i&&this._monthsParse[n].test(t))return n}},v.monthsRegex=function(t){return this._monthsParseExact?(f(this,"_monthsRegex")||qt.call(this),t?this._monthsStrictRegex:this._monthsRegex):(f(this,"_monthsRegex")||(this._monthsRegex=Rt),this._monthsStrictRegex&&t?this._monthsStrictRegex:this._monthsRegex)},v.monthsShortRegex=function(t){return this._monthsParseExact?(f(this,"_monthsRegex")||qt.call(this),t?this._monthsShortStrictRegex:this._monthsShortRegex):(f(this,"_monthsShortRegex")||(this._monthsShortRegex=Ft),this._monthsShortStrictRegex&&t?this._monthsShortStrictRegex:this._monthsShortRegex)},v.week=function(t){return Xt(t,this._week.dow,this._week.doy).week},v.firstDayOfYear=function(){return this._week.doy},v.firstDayOfWeek=function(){return this._week.dow},v.weekdays=function(t,e){return t?(u(this._weekdays)?this._weekdays:this._weekdays[this._weekdays.isFormat.test(e)?"format":"standalone"])[t.day()]:u(this._weekdays)?this._weekdays:this._weekdays.standalone},v.weekdaysMin=function(t){return t?this._weekdaysMin[t.day()]:this._weekdaysMin},v.weekdaysShort=function(t){return t?this._weekdaysShort[t.day()]:this._weekdaysShort},v.weekdaysParse=function(t,e,i){var n,o;if(this._weekdaysParseExact)return function(t,e,i){var n,o,s,t=t.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],n=0;n<7;++n)s=c([2e3,1]).day(n),this._minWeekdaysParse[n]=this.weekdaysMin(s,"").toLocaleLowerCase(),this._shortWeekdaysParse[n]=this.weekdaysShort(s,"").toLocaleLowerCase(),this._weekdaysParse[n]=this.weekdays(s,"").toLocaleLowerCase();return i?"dddd"===e?-1!==(o=A.call(this._weekdaysParse,t))?o:null:"ddd"===e?-1!==(o=A.call(this._shortWeekdaysParse,t))?o:null:-1!==(o=A.call(this._minWeekdaysParse,t))?o:null:"dddd"===e?-1!==(o=A.call(this._weekdaysParse,t))||-1!==(o=A.call(this._shortWeekdaysParse,t))||-1!==(o=A.call(this._minWeekdaysParse,t))?o:null:"ddd"===e?-1!==(o=A.call(this._shortWeekdaysParse,t))||-1!==(o=A.call(this._weekdaysParse,t))||-1!==(o=A.call(this._minWeekdaysParse,t))?o:null:-1!==(o=A.call(this._minWeekdaysParse,t))||-1!==(o=A.call(this._weekdaysParse,t))||-1!==(o=A.call(this._shortWeekdaysParse,t))?o:null}.call(this,t,e,i);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),n=0;n<7;n++){if(o=c([2e3,1]).day(n),i&&!this._fullWeekdaysParse[n]&&(this._fullWeekdaysParse[n]=new RegExp("^"+this.weekdays(o,"").replace(".",".?")+"$","i"),this._shortWeekdaysParse[n]=new RegExp("^"+this.weekdaysShort(o,"").replace(".",".?")+"$","i"),this._minWeekdaysParse[n]=new RegExp("^"+this.weekdaysMin(o,"").replace(".",".?")+"$","i")),this._weekdaysParse[n]||(o="^"+this.weekdays(o,"")+"|^"+this.weekdaysShort(o,"")+"|^"+this.weekdaysMin(o,""),this._weekdaysParse[n]=new RegExp(o.replace(".",""),"i")),i&&"dddd"===e&&this._fullWeekdaysParse[n].test(t))return n;if(i&&"ddd"===e&&this._shortWeekdaysParse[n].test(t))return n;if(i&&"dd"===e&&this._minWeekdaysParse[n].test(t))return n;if(!i&&this._weekdaysParse[n].test(t))return n}},v.weekdaysRegex=function(t){return this._weekdaysParseExact?(f(this,"_weekdaysRegex")||ne.call(this),t?this._weekdaysStrictRegex:this._weekdaysRegex):(f(this,"_weekdaysRegex")||(this._weekdaysRegex=te),this._weekdaysStrictRegex&&t?this._weekdaysStrictRegex:this._weekdaysRegex)},v.weekdaysShortRegex=function(t){return this._weekdaysParseExact?(f(this,"_weekdaysRegex")||ne.call(this),t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(f(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=ee),this._weekdaysShortStrictRegex&&t?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)},v.weekdaysMinRegex=function(t){return this._weekdaysParseExact?(f(this,"_weekdaysRegex")||ne.call(this),t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(f(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=ie),this._weekdaysMinStrictRegex&&t?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)},v.isPM=function(t){return"p"===(t+"").toLowerCase().charAt(0)},v.meridiem=function(t,e,i){return 11<t?i?"pm":"PM":i?"am":"AM"},he("en",{dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(t){var e=t%10;return t+(1===d(t%100/10)?"th":1==e?"st":2==e?"nd":3==e?"rd":"th")}}),p.lang=t("moment.lang is deprecated. Use moment.locale instead.",he),p.langData=t("moment.langData is deprecated. Use moment.localeData instead.",fe);var M=Math.abs;function ni(t,e,i,n){e=$(e,i);return t._milliseconds+=n*e._milliseconds,t._days+=n*e._days,t._months+=n*e._months,t._bubble()}function oi(t){return t<0?Math.floor(t):Math.ceil(t)}function si(t){return 4800*t/146097}function ri(t){return 146097*t/4800}function ai(t){return function(){return this.as(t)}}pt=ai("ms"),o=ai("s"),vt=ai("m"),ft=ai("h"),Mt=ai("d"),yt=ai("w"),gt=ai("M"),Nt=ai("y");function li(t){return function(){return this.isValid()?this._data[t]:NaN}}Tt=li("milliseconds"),Jt=li("seconds"),bt=li("minutes"),mt=li("hours"),Kt=li("days"),Bt=li("months"),v=li("years");var ci=Math.round,j={ss:44,s:45,m:45,h:22,d:26,M:11};function di(t,e,i){var n=$(t).abs(),o=ci(n.as("s")),s=ci(n.as("m")),r=ci(n.as("h")),a=ci(n.as("d")),l=ci(n.as("M")),n=ci(n.as("y")),o=(o<=j.ss?["s",o]:o<j.s&&["ss",o])||(s<=1?["m"]:s<j.m&&["mm",s])||(r<=1?["h"]:r<j.h&&["hh",r])||(a<=1?["d"]:a<j.d&&["dd",a])||(l<=1?["M"]:l<j.M&&["MM",l])||(n<=1?["y"]:["yy",n]);return o[2]=e,o[3]=0<+t,o[4]=i,function(t,e,i,n,o){return o.relativeTime(e||1,!!i,t,n)}.apply(null,o)}var ui=Math.abs;function hi(){var t,e,i,n,o,s,r;return this.isValid()?(s=ui(this._milliseconds)/1e3,i=ui(this._days),e=ui(this._months),o=a(s/60),n=a(o/60),s%=60,o%=60,t=a(e/12),e=e%=12,i=i,n=n,o=o,s=s,(r=this.asSeconds())?(r<0?"-":"")+"P"+(t?t+"Y":"")+(e?e+"M":"")+(i?i+"D":"")+(n||o||s?"T":"")+(n?n+"H":"")+(o?o+"M":"")+(s?s+"S":""):"P0D"):this.localeData().invalidDate()}var P=$e.prototype;return P.isValid=function(){return this._isValid},P.abs=function(){var t=this._data;return this._milliseconds=M(this._milliseconds),this._days=M(this._days),this._months=M(this._months),t.milliseconds=M(t.milliseconds),t.seconds=M(t.seconds),t.minutes=M(t.minutes),t.hours=M(t.hours),t.months=M(t.months),t.years=M(t.years),this},P.add=function(t,e){return ni(this,t,e,1)},P.subtract=function(t,e){return ni(this,t,e,-1)},P.as=function(t){if(!this.isValid())return NaN;var e,i,n=this._milliseconds;if("month"===(t=s(t))||"year"===t)return e=this._days+n/864e5,i=this._months+si(e),"month"===t?i:i/12;switch(e=this._days+Math.round(ri(this._months)),t){case"week":return e/7+n/6048e5;case"day":return e+n/864e5;case"hour":return 24*e+n/36e5;case"minute":return 1440*e+n/6e4;case"second":return 86400*e+n/1e3;case"millisecond":return Math.floor(864e5*e)+n;default:throw new Error("Unknown unit "+t)}},P.asMilliseconds=pt,P.asSeconds=o,P.asMinutes=vt,P.asHours=ft,P.asDays=Mt,P.asWeeks=yt,P.asMonths=gt,P.asYears=Nt,P.valueOf=function(){return this.isValid()?this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*d(this._months/12):NaN},P._bubble=function(){var t=this._milliseconds,e=this._days,i=this._months,n=this._data;return 0<=t&&0<=e&&0<=i||t<=0&&e<=0&&i<=0||(t+=864e5*oi(ri(i)+e),i=e=0),n.milliseconds=t%1e3,t=a(t/1e3),n.seconds=t%60,t=a(t/60),n.minutes=t%60,t=a(t/60),n.hours=t%24,e+=a(t/24),i+=t=a(si(e)),e-=oi(ri(t)),t=a(i/12),i%=12,n.days=e,n.months=i,n.years=t,this},P.get=function(t){return t=s(t),this.isValid()?this[t+"s"]():NaN},P.milliseconds=Tt,P.seconds=Jt,P.minutes=bt,P.hours=mt,P.days=Kt,P.weeks=function(){return a(this.days()/7)},P.months=Bt,P.years=v,P.humanize=function(t){var e,i;return this.isValid()?(e=this.localeData(),i=di(this,!t,e),t&&(i=e.pastFuture(+this,i)),e.postformat(i)):this.localeData().invalidDate()},P.toISOString=hi,P.toString=hi,P.toJSON=hi,P.locale=Ge,P.localeData=Ve,P.toIsoString=t("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",hi),P.lang=Qt,n("X",0,0,"unix"),n("x",0,0,"valueOf"),_("x",_t),_("X",/[+-]?\d+(\.\d{1,3})?/),k("X",function(t,e,i){i._d=new Date(1e3*parseFloat(t,10))}),k("x",function(t,e,i){i._d=new Date(d(t))}),p.version="2.18.1",N=O,p.fn=w,p.min=function(){return Le("isBefore",[].slice.call(arguments,0))},p.max=function(){return Le("isAfter",[].slice.call(arguments,0))},p.now=function(){return Date.now?Date.now():+new Date},p.utc=c,p.unix=function(t){return O(1e3*t)},p.months=function(t,e){return ei(t,e,"months")},p.isDate=F,p.locale=he,p.invalid=W,p.duration=$,p.isMoment=y,p.weekdays=function(t,e,i){return ii(t,e,i,"weekdays")},p.parseZone=function(){return O.apply(null,arguments).parseZone()},p.localeData=fe,p.isDuration=Me,p.monthsShort=function(t,e){return ei(t,e,"monthsShort")},p.weekdaysMin=function(t,e,i){return ii(t,e,i,"weekdaysMin")},p.defineLocale=pe,p.updateLocale=function(t,e){var i;return null!=e?(i=le,(i=new K(e=J(i=null!=L[t]?L[t]._config:i,e))).parentLocale=L[t],L[t]=i,he(t)):null!=L[t]&&(null!=L[t].parentLocale?L[t]=L[t].parentLocale:null!=L[t]&&delete L[t]),L[t]},p.locales=function(){return tt(L)},p.weekdaysShort=function(t,e,i){return ii(t,e,i,"weekdaysShort")},p.normalizeUnits=s,p.relativeTimeRounding=function(t){return void 0===t?ci:"function"==typeof t&&(ci=t,!0)},p.relativeTimeThreshold=function(t,e){return void 0!==j[t]&&(void 0===e?j[t]:(j[t]=e,"s"===t&&(j.ss=e-1),!0))},p.calendarFormat=function(t,e){return(t=t.diff(e,"days",!0))<-6?"sameElse":t<-1?"lastWeek":t<0?"lastDay":t<1?"sameDay":t<2?"nextDay":t<7?"nextWeek":"sameElse"},p.prototype=w,p}),!function(t,e){"object"==typeof exports&&"undefined"!=typeof module&&"function"==typeof require?e(require("../moment")):"function"==typeof define&&define.amd?define(["../moment"],e):e(t.moment)}(this,function(t){"use strict";return t.defineLocale("pt-br",{months:"Janeiro_Fevereiro_Março_Abril_Maio_Junho_Julho_Agosto_Setembro_Outubro_Novembro_Dezembro".split("_"),monthsShort:"Jan_Fev_Mar_Abr_Mai_Jun_Jul_Ago_Set_Out_Nov_Dez".split("_"),weekdays:"Domingo_Segunda-feira_Terça-feira_Quarta-feira_Quinta-feira_Sexta-feira_Sábado".split("_"),weekdaysShort:"Dom_Seg_Ter_Qua_Qui_Sex_Sáb".split("_"),weekdaysMin:"Do_2ª_3ª_4ª_5ª_6ª_Sá".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY [às] HH:mm",LLLL:"dddd, D [de] MMMM [de] YYYY [às] HH:mm"},calendar:{sameDay:"[Hoje às] LT",nextDay:"[Amanhã às] LT",nextWeek:"dddd [às] LT",lastDay:"[Ontem às] LT",lastWeek:function(){return 0===this.day()||6===this.day()?"[Último] dddd [às] LT":"[Última] dddd [às] LT"},sameElse:"L"},relativeTime:{future:"em %s",past:"%s atrás",s:"poucos segundos",m:"um minuto",mm:"%d minutos",h:"uma hora",hh:"%d horas",d:"um dia",dd:"%d dias",M:"um mês",MM:"%d meses",y:"um ano",yy:"%d anos"},dayOfMonthOrdinalParse:/\d{1,2}º/,ordinal:"%dº"})}),!function(t,e){"object"==typeof exports&&"undefined"!=typeof module&&"function"==typeof require?e(require("../moment")):"function"==typeof define&&define.amd?define(["../moment"],e):e(t.moment)}(this,function(t){"use strict";var i="ene._feb._mar._abr._may._jun._jul._ago._sep._oct._nov._dic.".split("_"),n="ene_feb_mar_abr_may_jun_jul_ago_sep_oct_nov_dic".split("_");return t.defineLocale("es",{months:"enero_febrero_marzo_abril_mayo_junio_julio_agosto_septiembre_octubre_noviembre_diciembre".split("_"),monthsShort:function(t,e){return t?(/-MMM-/.test(e)?n:i)[t.month()]:i},monthsParseExact:!0,weekdays:"domingo_lunes_martes_miércoles_jueves_viernes_sábado".split("_"),weekdaysShort:"dom._lun._mar._mié._jue._vie._sáb.".split("_"),weekdaysMin:"do_lu_ma_mi_ju_vi_sá".split("_"),weekdaysParseExact:!0,longDateFormat:{LT:"H:mm",LTS:"H:mm:ss",L:"DD/MM/YYYY",LL:"D [de] MMMM [de] YYYY",LLL:"D [de] MMMM [de] YYYY H:mm",LLLL:"dddd, D [de] MMMM [de] YYYY H:mm"},calendar:{sameDay:function(){return"[hoy a la"+(1!==this.hours()?"s":"")+"] LT"},nextDay:function(){return"[mañana a la"+(1!==this.hours()?"s":"")+"] LT"},nextWeek:function(){return"dddd [a la"+(1!==this.hours()?"s":"")+"] LT"},lastDay:function(){return"[ayer a la"+(1!==this.hours()?"s":"")+"] LT"},lastWeek:function(){return"[el] dddd [pasado a la"+(1!==this.hours()?"s":"")+"] LT"},sameElse:"L"},relativeTime:{future:"en %s",past:"hace %s",s:"unos segundos",m:"un minuto",mm:"%d minutos",h:"una hora",hh:"%d horas",d:"un día",dd:"%d días",M:"un mes",MM:"%d meses",y:"un año",yy:"%d años"},dayOfMonthOrdinalParse:/\d{1,2}º/,ordinal:"%dº",week:{dow:1,doy:4}})}),{modal_id:null,form_id:null,url:"/",method:"POST",submit_btn_id:null,has_captcha:!1,captcha_key:null,captcha_theme:"light",captcha_id:null,email_confirm_modal_id:"#modal_confirm",success_message:null,error_message:null,rcaptcha:null,submit:function(){var e=this;$.ajax({type:e.method,url:e.url,data:$(e.form_id).serialize(),success:function(t){$.each(t.fields,function(t,e){$("#"+e).parent().removeClass("has-error"),$("#"+e+"_error").html("")}),!1===t.sent?(!0===e.has_captcha&&($(e.submit_btn_id).attr("disabled","disabled"),e.render_captcha()),$.each(t.message,function(t,e){$("#"+t).parent().addClass("has-error"),$("#"+t+"_error").html(e)})):($(e.modal_id).modal("toggle"),$(".midGlyph").addClass("success"),$(".midGlyph").html(e.success_message),$(e.email_confirm_modal_id).modal("show"))},error:function(t){$(e.modal_id).modal("toggle"),$(".midGlyph").removeClass("success").toggleClass("unsuccess"),$(".midGlyph").html(e.error_message),$(e.email_confirm_modal_id).modal("show")}})},recaptcha_callback:function(){$(this.submit_btn_id).removeAttr("disabled")},registry_recaptcha_modal:function(){var t=this;$(this.modal_id).on("show.bs.modal",function(){setTimeout(function(){t.render_captcha(t.captcha_id,t.captcha_key,t.captcha_theme)},100)})},render_captcha:function(t,e,i){this.render_captcha&&$(this.submit_btn_id).attr("disabled","disabled"),null===this.rcaptcha?this.rcaptcha=grecaptcha.render(t,{sitekey:e,theme:i,callback:this.recaptcha_callback.bind(this)}):grecaptcha.reset(this.rcaptcha)},init:function(t,e,i,n,o,s,r,a,l,c,d,u){this.modal_id=t,this.form_id=e,this.url=i,this.method=n,this.submit_btn_id=o,this.has_captcha=s,this.captcha_key=r,this.captcha_theme=l,this.captcha_id=a,this.email_confirm_modal_id=c,this.success_message=d,this.error_message=u,!(this.rcaptcha=null)===this.has_captcha&&this.registry_recaptcha_modal();var h=this;return $(this.form_id).submit(function(t){t.preventDefault(),h.submit()}),this}}); +//# sourceMappingURL=../maps/scielo-bundle-min.js.map \ No newline at end of file diff --git a/manuscripts/static/xsl/jats-html.xsl b/manuscripts/static/xsl/jats-html.xsl new file mode 100644 index 0000000..e35215a --- /dev/null +++ b/manuscripts/static/xsl/jats-html.xsl @@ -0,0 +1,4028 @@ +<?xml version="1.0"?> +<!-- ============================================================= --> +<!-- MODULE: HTML Preview of NISO JATS Publishing 1.0 XML --> +<!-- DATE: May-June 2012 --> +<!-- --> +<!-- ============================================================= --> + +<!-- ============================================================= --> +<!-- SYSTEM: NCBI Archiving and Interchange Journal Articles --> +<!-- --> +<!-- PURPOSE: Provide an HTML preview of a journal article, --> +<!-- in a form suitable for reading. --> +<!-- --> +<!-- PROCESSOR DEPENDENCIES: --> +<!-- None: standard XSLT 1.0 --> +<!-- Tested using Saxon 6.5, Tranformiix (Firefox), --> +<!-- Saxon 9.4.0.3 --> +<!-- --> +<!-- COMPONENTS REQUIRED: --> +<!-- 1) This stylesheet --> +<!-- 2) CSS styles defined in jats-preview.css --> +<!-- (to be placed with the results) --> +<!-- --> +<!-- INPUT: An XML document valid to (any of) the --> +<!-- NISO JATS 1.0, NLM/NCBI Journal Publishing 3.0, --> +<!-- or NLM/NCBI Journal Publishing 2.3 DTDs. --> +<!-- (May also work with older variants, --> +<!-- and note further assumptions and limitations --> +<!-- below.) --> +<!-- --> +<!-- OUTPUT: HTML (XHTML if a postprocessor is used) --> +<!-- --> +<!-- CREATED FOR: --> +<!-- Digital Archive of Journal Articles --> +<!-- National Center for Biotechnology Information (NCBI) --> +<!-- National Library of Medicine (NLM) --> +<!-- --> +<!-- CREATED BY: --> +<!-- Wendell Piez (based on HTML design by --> +<!-- Kate Hamilton and Debbie Lapeyre, 2004), --> +<!-- Mulberry Technologies, Inc. --> +<!-- --> +<!-- ============================================================= --> + +<!-- ============================================================= --> +<!-- + This work is in the public domain and may be reproduced, published or + otherwise used without the permission of the National Library of Medicine (NLM). + + We request only that the NLM is cited as the source of the work. + + Although all reasonable efforts have been taken to ensure the accuracy and + reliability of the software and data, the NLM and the U.S. Government do + not and cannot warrant the performance or results that may be obtained by + using this software or data. The NLM and the U.S. Government disclaim all + warranties, express or implied, including warranties of performance, + merchantability or fitness for any particular purpose. +--> +<!-- ============================================================= --> + +<!-- Change history --> + +<!-- From jpub3-html.xsl v3.0 to jats-html.xsl v1.0: + +Calls to 'normalize-space($node)' where $node is not a string are +expressed as 'normalize-space(string($node)) in order to provide +type safety in some XSLT 2.0 processors. + +Support for certain elements in NLM Blue v2.3 is added to provide +backward compatibility: + floats-wrap (same as floats-group) + chem-struct-wrapper (same as chem-struct-wrap) + custom-meta-wrap (same as custom-meta-group) + floats-wrap (same as floats-group) + gloss-group (same as glossary) + citation + contract-num + contract-sponsor + grant-num + grant-sponsor + +Support is added for looser structures for title-group elements +in 2.3 (title, trans-title, trans-subtitle etc.) Same for 2.3 +tagging of permissions info (copyright-statement, copyright-year, +license) and funding/contract info (contract-num, contract-sponsor, +grant-num, grant-sponsor). + +Elements newly permitted in JATS journal-meta +(contrib-group, aff, aff-alternatives) are supported. + +New elements in NISO JATS v1.0 are supported: + aff-alternatives + citation-alternatives + collab-alternatives + trans-title-group (with @xml:lang) + contrib-id + count> + issn-l + nested-kwd + +Added support for @pub-id-type='arXiv' + +Named anchor logic extended to support "alternatives" wrappers +for aff, contrib, citation etc. + +Footer text is emended, with name of transformation (stylesheet +or pipeline) parameterized. + +--> +<xsl:stylesheet version="1.0" + xmlns:xsl="http://www.w3.org/1999/XSL/Transform" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:mml="http://www.w3.org/1998/Math/MathML" + exclude-result-prefixes="xlink mml"> + + + <!--<xsl:output method="xml" indent="no" encoding="UTF-8" + doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN" + doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"/>--> + + + <xsl:output doctype-public="-//W3C//DTD HTML 4.01 Transitional//EN" + doctype-system="http://www.w3.org/TR/html4/loose.dtd" + encoding="UTF-8"/> + + <xsl:strip-space elements="*"/> + + <!-- Space is preserved in all elements allowing #PCDATA --> + <xsl:preserve-space + elements="abbrev abbrev-journal-title access-date addr-line + aff alt-text alt-title article-id article-title + attrib award-id bold chapter-title chem-struct + collab comment compound-kwd-part compound-subject-part + conf-acronym conf-date conf-loc conf-name conf-num + conf-sponsor conf-theme contrib-id copyright-holder + copyright-statement copyright-year corresp country + date-in-citation day def-head degrees disp-formula + edition elocation-id email etal ext-link fax fpage + funding-source funding-statement given-names glyph-data + gov inline-formula inline-supplementary-material + institution isbn issn-l issn issue issue-id issue-part + issue-sponsor issue-title italic journal-id + journal-subtitle journal-title kwd label license-p + long-desc lpage meta-name meta-value mixed-citation + monospace month named-content object-id on-behalf-of + overline p page-range part-title patent person-group + phone prefix preformat price principal-award-recipient + principal-investigator product pub-id publisher-loc + publisher-name related-article related-object role + roman sans-serif sc season self-uri series series-text + series-title sig sig-block size source speaker std + strike string-name styled-content std-organization + sub subject subtitle suffix sup supplement surname + target td term term-head tex-math textual-form th + time-stamp title trans-source trans-subtitle trans-title + underline uri verse-line volume volume-id volume-series + xref year + + mml:annotation mml:ci mml:cn mml:csymbol mml:mi mml:mn + mml:mo mml:ms mml:mtext"/> + + + <xsl:param name="transform" select="'jats-html.xsl'"/> + + <xsl:param name="css" select="'jats-preview.css'"/> + + <xsl:param name="report-warnings" select="'no'"/> + + <xsl:variable name="verbose" select="$report-warnings='yes'"/> + + <!-- Keys --> + + <!-- To reduce dependency on a DTD for processing, we declare + a key to use instead of the id() function. --> + <xsl:key name="element-by-id" match="*[@id]" use="@id"/> + + <!-- Enabling retrieval of cross-references to objects --> + <xsl:key name="xref-by-rid" match="xref" use="@rid"/> + + <!-- ============================================================= --> + <!-- ROOT TEMPLATE - HANDLES HTML FRAMEWORK --> + <!-- ============================================================= --> + + <xsl:template match="/"> + <html> + <!-- HTML header --> + <xsl:call-template name="make-html-header"/> + <body> + <xsl:apply-templates/> + </body> + </html> + </xsl:template> + + + <xsl:template name="make-html-header"> + <head> + <title> + <xsl:variable name="authors"> + <xsl:call-template name="author-string"/> + </xsl:variable> + <xsl:value-of select="normalize-space(string($authors))"/> + <xsl:if test="normalize-space(string($authors))">: </xsl:if> + <xsl:value-of + select="/article/front/article-meta/title-group/article-title[1]"/> + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + +
+ +
+ + + +
+ +
+
+ + + +
+ +
+
+ + + +
+ + + Floating objects + + + +
+
+ + + + + + + +
+ + + + + +
+ + + + + + + + +
+ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [ + + ] + + + + + + + + + + , + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +


+ + + + + + + + + + + + + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [Private character + + + + + ] + + + + + + (Glyph not rendered) + + + + + [Related article:] + + + + + + [Related object:] + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Notes + + + + + + + + + + Acknowledgements + + + + + + + Appendices + + + + + + + Biography + + + + + + + Notes + + + + + + + Glossary + + + + + + + References + + + + + + + + + + Notes + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + +

+ + + + + + + +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Box + + + + + + + + + + + + + + Formula + + + + + + + + + + + + + + Formula (chemical) + + + + + + + + + + + + + + Figure + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + [ + + ] + + + + + + + + + + + + + + + + [ + + ] + + + + + + + Abbreviation + + + + Communicated by + + + + Contributed by + + + + Conflicts of interest + + + + Corresponding author + + + + Current affiliation + + + + Deceased + + + + Edited by + + + + Equal contributor + + + + Financial disclosure + + + + On leave + + + + + + Participating researcher + + + + Current address + + + + Presented at + + + + Presented by + + + + Previously at + + + + Study group member + + + + Supplementary material + + + + Supported by + + + + + + + + + + + + + + + + + + + + + + + + + + Statement + + + + + + + + + + + + + + Supplementary Material + + + + + + + + + + + + + + Table + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ( + + + electronic + print + print and electronic + electronic preprint + print preprint + corrected, electronic + corrected, print + retracted, electronic + retracted, print + + + + + + ) + + + + + + + + + + + + + + + : + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { label + (or @symbol) + needed for + + + [@id=' + + '] + + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + named anchor + + + + + + + + + + + + + + + + + + + + + + +
  • + + + +
  • +
    +
    +
    + + +

    Elements are cross-referenced without labels.

    +

    Either the element should be provided a label, or their cross-reference(s) should + have literal text content.

    +
      + +
    +
    + + + + + +
  • + In + +
      + +
    • + +
    • +
      +
    + +
  • +
    +
    + + +

    Elements with different 'specific-use' assignments appearing together

    +
      + +
    +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + January + February + March + April + May + June + July + August + September + October + November + December + + + + + + + + + + + + + + + + + , + + + and + + + + + + + + + + + +
    +
    +

    + This display is generated from + NISO JATS XML with + + + + . The XSLT engine is + + . +

    +
    +
    + + + + + + + + + + + + + + + + [@ + + =' + + '] + + + + + + + + + + + + + + + + / + + + [ + + ] + + + + + + /@ + + + + + + /comment() + + [ + + ] + + + + + + /processing-instruction() + + [ + + ] + + + + + + /text() + + [ + + ] + + + + + + + + + diff --git a/manuscripts/static/xsl/xml-tree.xsl b/manuscripts/static/xsl/xml-tree.xsl new file mode 100644 index 0000000..35bb61d --- /dev/null +++ b/manuscripts/static/xsl/xml-tree.xsl @@ -0,0 +1,103 @@ + + + + + + + + + + + XML Viewer + + + + +
    + + +
    +
    + + +
    + + < + +  = + "" + + > + + +
    </>
    +
    +
    +
    + + +
    + + + +
    + + < + +  = + "" + + > + + +
    </>
    +
    +
    + + + + + +
    +
    +
    + + + +
    <!-- -->
    +
    + + +
    + diff --git a/manuscripts/structure.py b/manuscripts/structure.py new file mode 100644 index 0000000..f53f39d --- /dev/null +++ b/manuscripts/structure.py @@ -0,0 +1,95 @@ +import re + +from django.db import transaction + +from ai.utils.normalizers import stz_norm +from references.data_utils import get_reference +from references.models import Reference, ReferenceStatus + +from manuscripts.models.article import ArticleReference, ArticleStructureVersion, CitationOccurrence +from manuscripts.utils.helpers import ( + XREF_RE, + checksum_bytes, + json_safe, + to_dict_list, +) + + +def create_structure_version(article, processing, source_kind, front, body, back, base_xml="", warnings=None, xref_status=None): + front_raw = to_dict_list(front) + body_raw = to_dict_list(body) + back_raw = to_dict_list(back) + with transaction.atomic(): + current = article.structure_versions.select_for_update().filter(is_current=True) + version = (current.order_by("-version").values_list("version", flat=True).first() or 0) + 1 + current.update(is_current=False) + structure = ArticleStructureVersion.objects.create( + article=article, + processing=processing, + version=version, + is_current=True, + source_kind=source_kind, + front=json_safe(front_raw), + body=json_safe(body_raw), + back=json_safe(back_raw), + base_xml=base_xml, + roundtrip_warnings=json_safe(warnings or []), + xref_status=json_safe(xref_status or {}), + creator=processing.creator, + ) + sync_references(structure) + return structure + + +def sync_references(structure): + structure.references.all().delete() + structure.citations.all().delete() + by_id = {} + back_raw = to_dict_list(structure.back) + for position, block in enumerate(back_raw, 1): + value = block.get("value", {}) + if block.get("type") != "ref_paragraph": + continue + text = value.get("paragraph") or "" + ref_id = value.get("refid") or f"B{position}" + normalized = stz_norm(text) + checksum = checksum_bytes(normalized.encode("utf-8")) + global_reference = Reference.objects.filter(checksum=checksum).first() + created = False + if not global_reference: + global_reference = Reference.objects.create( + mixed_citation=text, + status=ReferenceStatus.CREATING, + creator=structure.creator, + ) + created = True + local = ArticleReference.objects.create( + structure=structure, + reference=global_reference, + position=position, + ref_id=ref_id, + mixed_citation=text, + metadata=value, + creator=structure.creator, + ) + by_id[ref_id] = local + if created: + transaction.on_commit(lambda pk=global_reference.pk: get_reference.delay(pk)) + + body_raw = to_dict_list(structure.body) + for block_index, block in enumerate(body_raw): + text = block.get("value", {}).get("paragraph") or "" + for match in XREF_RE.finditer(text): + refs = [by_id[rid] for rid in match.group(1).split() if rid in by_id] + occurrence = CitationOccurrence.objects.create( + structure=structure, + text=re.sub("<[^>]+>", "", match.group(2)), + location={"section": "body", "block": block_index}, + status=( + CitationOccurrence.Status.LINKED + if refs + else CitationOccurrence.Status.ORPHAN + ), + creator=structure.creator, + ) + occurrence.references.set(refs) diff --git a/manuscripts/tasks.py b/manuscripts/tasks.py new file mode 100644 index 0000000..c3c315d --- /dev/null +++ b/manuscripts/tasks.py @@ -0,0 +1,8 @@ +from config import celery_app + +from . import processing + + +@celery_app.task(bind=True) +def process_input(self, processing_id, start_action=None): + processing.process_input(self, processing_id, start_action) diff --git a/manuscripts/templates/manuscripts/article_inspect.html b/manuscripts/templates/manuscripts/article_inspect.html new file mode 100644 index 0000000..ee1d77a --- /dev/null +++ b/manuscripts/templates/manuscripts/article_inspect.html @@ -0,0 +1,426 @@ +{% extends "wagtailadmin/generic/inspect.html" %} +{% load i18n %} + +{% block extra_css %} + {{ block.super }} + +{% endblock %} + +{% block fields_output %} +
    +

    {% trans "Article data" %}

    +
    +
    {% trans "Provisional title" %}
    +
    {{ object.title|default:"-" }}
    +
    DOI
    +
    {{ object.doi|default:"-" }}
    +
    PID
    +
    {{ object.pid|default:"-" }}
    +
    Journal
    +
    + {% if object.journal %} + {{ object.journal }} + {% else %} + {% trans "Undetermined" %} + {% endif %} +
    +
    Issue
    +
    + {% if object.issue %} + {{ object.issue }} + {% else %} + {% trans "Undetermined" %} + {% endif %} +
    +
    {% trans "Status" %}
    +
    {{ object.get_status_display }}
    +
    {% trans "Created at" %}
    +
    {{ object.created|date:"SHORT_DATETIME_FORMAT" }}
    +
    {% trans "Updated at" %}
    +
    {{ object.updated|date:"SHORT_DATETIME_FORMAT" }}
    +
    +
    + +
    +

    {% trans "Article actions" %}

    +
    + {% if structure %} + {% trans "Edit structure" %} + {% trans "Edit references and citations" %} + {% endif %} + {% if current_validation_report_artifact %} + {% trans "View validation" %} + {% endif %} + {% if current_html_artifact %} + {% trans "Preview HTML" %} + {% endif %} +
    + {% trans "Downloads" %} +
    + {% if current_pdf_artifact %} + {% trans "PDF" %} + {% endif %} + {% if current_xml_artifact %} + {% trans "XML" %} + {% endif %} + {% if current_sps_package_artifact %} + {% trans "SPS Package" %} + {% endif %} + {% if current_marked_document_artifact %} + {% trans "Marked DOCX" %} + {% endif %} + {% if not current_pdf_artifact and not current_xml_artifact and not current_sps_package_artifact and not current_marked_document_artifact %} + {% trans "No current file" %} + {% endif %} +
    +
    +
    + + {% if latest_processing %} +
    +
    + {% csrf_token %} + + +
    +
    +
    +

    + {% trans "Edited structure" %}: {% trans "to use manual edits, start at" %} {% trans "Generate XML" %} {% trans "or a later step." %} +

    +
    +
    +

    + {% trans "Attention" %}: {% trans "From the start or Mark citations re-reads the original file and may overwrite manual structure edits." %} +

    +
    +
    +
    + {% endif %} + +
    + {% trans "Clear generated" %} +
    +

    + {% trans "Irreversible action." %} + {% trans "Removes artifacts, files, structural versions, local references/citations, and events from this article. The article record remains." %} +

    +
    + {% csrf_token %} + + +
    +
    +
    +
    + + {% if structure %} +
    +

    {% trans "Structure, references and citations" %}

    +
      +
    • v{{ structure.version }}{% trans "Current structure" %}
    • +
    • {{ structure.front|length }}front
    • +
    • {{ structure.body|length }}body
    • +
    • {{ structure.back|length }}back
    • +
    • {{ references|length }}{% trans "references" %}
    • +
    • {{ citations|length }}{% trans "citations" %}
    • +
    + {% if structure.roundtrip_warnings %}

    {{ structure.roundtrip_warnings }}

    {% endif %} +
    + {% endif %} + +
    +

    {% trans "Related processings" %}

    + + + + + + + + + + {% for row in processing_rows %} + {% with processing=row.processing ai=row.frontmatter_ai %} + + + + + + {% endwith %} + {% empty %} + + {% endfor %} + +
    {% trans "Processing" %}{% trans "Status" %}{% trans "AI" %}
    {{ processing }}{{ processing.get_status_display }} + {% if ai and ai.provider %} + {{ ai.provider }} + {% else %} + - + {% endif %} +
    {% trans "No related processing." %}
    +
    + +
    +

    {% trans "Products and inputs" %}

    +

    {% trans "Current products" %}

    + + + + + + + + + + + + + {% for row in current_artifact_rows %} + {% with artifact=row.artifact %} + + + + + + + + + {% endwith %} + {% empty %} + + {% endfor %} + +
    {% trans "Type" %}{% trans "Structure" %}{% trans "Product" %}{% trans "Created at" %}{% trans "Processing" %}{% trans "File" %}
    {{ artifact.get_artifact_type_display }}{% if row.structure %}v{{ row.structure.version }}{% else %}-{% endif %}v{{ artifact.version }}{{ artifact.created|date:"SHORT_DATETIME_FORMAT" }}{{ artifact.processing }}{{ row.filename }}
    {% trans "No current product." %}
    + +
    + {% trans "View product history" %} ({{ historical_artifact_rows|length }}) + + + + + + + + + + + + + {% for row in historical_artifact_rows %} + {% with artifact=row.artifact %} + + + + + + + + + {% endwith %} + {% empty %} + + {% endfor %} + +
    {% trans "Created at" %}{% trans "Type" %}{% trans "Structure" %}{% trans "Product" %}{% trans "Status" %}{% trans "File" %}
    {{ artifact.created|date:"SHORT_DATETIME_FORMAT" }}{{ artifact.get_artifact_type_display }}{% if row.structure %}v{{ row.structure.version }}{% else %}-{% endif %}v{{ artifact.version }}{% if artifact.is_stale %}{% trans "outdated" %}{% else %}{% trans "historical" %}{% endif %}{{ row.filename }}
    {% trans "No historical product." %}
    +
    +
    +{% endblock %} diff --git a/manuscripts/templates/manuscripts/article_references_edit.html b/manuscripts/templates/manuscripts/article_references_edit.html new file mode 100644 index 0000000..ad1f225 --- /dev/null +++ b/manuscripts/templates/manuscripts/article_references_edit.html @@ -0,0 +1,92 @@ +{% extends "wagtailadmin/base.html" %} +{% load i18n %} + +{% block titletag %}{% trans "Edit references and citations" %}: {{ article }}{% endblock %} + +{% block content %} + {% include "wagtailadmin/shared/header.html" with title="Edit references and citations" subtitle=article icon="link" %} + +
    +
    +

    {% trans "Article actions" %}

    + +

    + {% blocktrans with version=structure.version refs=references|length cits=citations|length %}Structure v{{ version }}: {{ refs }} references and {{ cits }} citation occurrences.{% endblocktrans %} +

    +
    + +

    {% trans "References" %}

    + + + + + + + + + + + {% for ref in references %} + + + + + + + {% empty %} + + {% endfor %} + +
    ID{% trans "Reference" %}{% trans "Global base" %}{% trans "Interpretation" %}
    {{ ref.ref_id }}{{ ref.mixed_citation }}{{ ref.reference.get_status_display }} + {% if ref.reference.element_citation.exists %} +
    + {% csrf_token %} + + +
    + {% else %} + {% trans "No candidates" %} + {% endif %} +
    {% trans "No references." %}
    + +

    {% trans "Citations" %}

    + + + + + + + + + + {% for citation in citations %} + + + + + + {% empty %} + + {% endfor %} + +
    {% trans "Citation" %}{% trans "Status" %}{% trans "Linked references" %}
    {{ citation.text }}{{ citation.get_status_display }} +
    + {% csrf_token %} + + +
    +
    {% trans "No occurrences." %}
    +
    +{% endblock %} diff --git a/manuscripts/templates/manuscripts/article_structure_edit.html b/manuscripts/templates/manuscripts/article_structure_edit.html new file mode 100644 index 0000000..3c2c7df --- /dev/null +++ b/manuscripts/templates/manuscripts/article_structure_edit.html @@ -0,0 +1,130 @@ +{% extends "wagtailadmin/base.html" %} +{% load i18n wagtailadmin_tags %} +{% block titletag %}{% trans "Edit structure" %}: {{ article }}{% endblock %} + +{% block extra_css %} + {{ block.super }} + {{ form.media.css }} + +{% endblock %} + +{% block extra_js %} + {{ block.super }} + {{ form.media.js }} +{% endblock %} + +{% block content %} + {% include "wagtailadmin/shared/header.html" with title="Edit article structure" icon="edit" %} +
    + {% if is_historical_version %} +
    + {% blocktrans with version=structure.version current_version=current_structure.version %}You are editing a historical version (v{{ version }}). The current version is v{{ current_version }}. Saving will create a new version from this content.{% endblocktrans %} +
    + {% endif %} +
    + {% csrf_token %} + {{ form.media }} + {% for field in form %} +
    + {{ field.label }} +
    {{ field }}
    +
    + {% endfor %} +
    + + {% if xml_artifact %} + {% trans "View current XML" %} + {% endif %} +
    +
    +
    + +
    +
    +

    {% trans "Version history" %}

    + + + + + + + + + + + + {% for version_item in versions %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
    {% trans "Version" %}{% trans "Status" %}{% trans "Origin" %}{% trans "Created at" %}{% trans "Actions" %}
    v{{ version_item.version }} + {% if version_item.is_current %} + {% trans "Current" %} + {% else %} + {% trans "Historical" %} + {% endif %} + {{ version_item.get_source_kind_display }}{{ version_item.created|date:"SHORT_DATETIME_FORMAT" }} +
    + + {% trans "Load in editor" %} + + {% if not version_item.is_current %} +
    + {% csrf_token %} + +
    + {% endif %} +
    +
    {% trans "No structural version recorded." %}
    +
    +
    +{% endblock %} diff --git a/manuscripts/templates/manuscripts/article_validation.html b/manuscripts/templates/manuscripts/article_validation.html new file mode 100644 index 0000000..9125aba --- /dev/null +++ b/manuscripts/templates/manuscripts/article_validation.html @@ -0,0 +1,325 @@ +{% extends "wagtailadmin/base.html" %} +{% load i18n %} + +{% block titletag %}{% trans "Validation" %}: {{ article }}{% endblock %} + +{% block extra_css %} + {{ block.super }} + {# CodeMirror via CDN for annotated XML #} + + + +{% endblock %} + +{% block extra_js %} + {{ block.super }} + + + + +{% endblock %} + +{% block content %} + {% include "wagtailadmin/shared/header.html" with title=article subtitle=_("Validation report") icon="tick-inverse" %} +
    + + {# Download links #} + + + {# Summary cards #} +
    +
    +
    {{ rows|length }}
    +
    {% trans "Data issues" %}
    +
    +
    +
    {{ exceptions|length }}
    +
    {% trans "Exceptions" %}
    +
    + {% if schema_valid is not None %} +
    +
    {{ schema_errors|length }}
    +
    {% trans "Schema errors" %}
    +
    + {% endif %} +
    + + {# Tabs #} +
    +
    {% trans "Data validation" %} ({{ rows|length }})
    + {% if xml_artifact %} +
    {% trans "Schema DTD/SPS" %} {% if schema_valid is not None %}({{ schema_errors|length }}){% endif %}
    + {% endif %} +
    + + {# ── TAB: Data ── #} +
    + {% if rows %} +
    + + +
    +
    + + + + + + + + + + + + + + + {% for row in rows %} + + + + + + + + + + + {% endfor %} + +
    {% trans "Group" %}{% trans "Title" %}{% trans "Attribute" %}{% trans "Type" %}{% trans "Resp." %}{% trans "Expected" %}{% trans "Got" %}{% trans "Advice" %}
    {{ row.group|default:"—" }}{{ row.title|default:"—" }}{{ row.attribute|default:"—" }}{{ row.validation_type|default:"—" }}{{ row.response }}{{ row.expected_value|default:"—" }}{{ row.got_value|default:"—" }}{{ row.advice|default:"" }}
    +
    + {% else %} +

    {% trans "No data validation issues found." %}

    + {% endif %} + + {% if exceptions %} +
    + {% trans "Technical exceptions" %} ({{ exceptions|length }}) +
    {{ exceptions|pprint }}
    +
    + {% endif %} +
    + + {# ── TAB: Schema DTD/SPS ── #} + {% if xml_artifact %} +
    + {% if schema_valid is None %} +

    {% trans "Schema validation could not be performed." %} + {% if schema_errors %} — {{ schema_errors.0.message }}{% endif %} +

    + {% else %} + {% if schema_valid %} + {% trans "XML valid according to DTD/SPS schema" %} + {% else %} + {% trans "XML invalid — see errors below" %} + {% endif %} + + {% if schema_errors %} +
      + {% for err in schema_errors %} +
    • + {% if err.line %}L{{ err.line }}{% else %}—{% endif %} + {{ err.message }} +
    • + {% endfor %} +
    + {% endif %} + + {% if annotated_xml %} +

    + {% trans "XML with embedded error annotations (XML comments):" %} +

    + + {% endif %} + {% endif %} +
    + {% endif %} + +
    + + +{% endblock %} diff --git a/manuscripts/templates/manuscripts/processing_inspect.html b/manuscripts/templates/manuscripts/processing_inspect.html new file mode 100644 index 0000000..9d01eab --- /dev/null +++ b/manuscripts/templates/manuscripts/processing_inspect.html @@ -0,0 +1,183 @@ +{% extends "wagtailadmin/generic/inspect.html" %} +{% load i18n %} +{% block fields_output %} + + {{ block.super }} + +
    +

    {% trans "Processing actions" %}

    +
    + {% if object.status == "awaiting_review" %} + {% trans "Review and start" %} + {% endif %} +
    +
    +
    +

    + {% trans "Reprocessing" %}: {% trans "Choose the starting step to re-run the trail from it." %} +

    +
    +
    + {% csrf_token %} + + +
    +
    +
    + {% trans "Clear artifacts" %} +
    +

    + {% trans "Irreversible action." %} + {% trans "Removes artifacts, files, structural versions, and events generated by this processing. The processing record remains." %} +

    +
    + {% csrf_token %} + + + +
    +
    +
    +
    + +

    {% trans "Articles" %}

    +
    + {% for article in articles %} + {{ article }} + {% empty %} + {% trans "No article yet." %} + {% endfor %} +
    + +

    {% trans "Trail" %}

    + + + + {% for event in events %} + + + + + + + + {% empty %} + + {% endfor %} + +
    {% trans "Action" %}{% trans "Article" %}{% trans "Status" %}{% trans "AI" %}{% trans "Message" %}
    {{ event.get_action_display }}{{ event.article|default:"-" }}{{ event.get_status_display }} + {% with ai=event.details.frontmatter_ai %} + {% if ai and ai.provider %} + {{ ai.provider }} + {% else %} + - + {% endif %} + {% endwith %} + {{ event.message }}
    {% trans "Awaiting execution." %}
    + +

    {% trans "Artifacts" %}

    + + + + {% for row in artifact_rows %} + {% with artifact=row.artifact %} + + + + + + + + {% endwith %} + {% empty %} + + {% endfor %} + +
    {% trans "Type" %}{% trans "Product" %}{% trans "Status" %}{% trans "Article" %}{% trans "File" %}
    {{ artifact.get_artifact_type_display }}v{{ artifact.version }} + {% if artifact.is_current %}{% trans "Current" %}{% else %}{% trans "Historical" %}{% endif %} + {% if artifact.is_stale %} · {% trans "outdated" %}{% endif %} + + {% if artifact.article %} + {{ artifact.article }} + {% else %}-{% endif %} + {{ row.filename }}
    {% trans "No artifacts." %}
    +{% endblock %} diff --git a/manuscripts/templates/manuscripts/processing_review.html b/manuscripts/templates/manuscripts/processing_review.html new file mode 100644 index 0000000..d431aac --- /dev/null +++ b/manuscripts/templates/manuscripts/processing_review.html @@ -0,0 +1,17 @@ +{% extends "wagtailadmin/base.html" %} +{% load i18n %} +{% block titletag %}{% trans "Review processing" %}{% endblock %} +{% block content %} + {% include "wagtailadmin/shared/header.html" with title="Review processing" icon="tasks" %} +
    +

    {% trans "File" %}: {{ processing.input_file.name }}

    + {% if processing.inspection.article_candidates %} + + {% for article in processing.inspection.article_candidates %}{% endfor %} +
    {% trans "File" %}{% trans "Title" %}DOIPID
    {{ article.path|default:"-" }}{{ article.title|default:"-" }}{{ article.doi|default:"-" }}{{ article.pid|default:"-" }}
    + {% endif %} +
    {% csrf_token %}{{ form.as_p }} + +
    +
    +{% endblock %} diff --git a/manuscripts/templates/wagtailadmin/icons/doc-docx.svg b/manuscripts/templates/wagtailadmin/icons/doc-docx.svg new file mode 100644 index 0000000..5714991 --- /dev/null +++ b/manuscripts/templates/wagtailadmin/icons/doc-docx.svg @@ -0,0 +1,3 @@ + + + diff --git a/manuscripts/templates/wagtailadmin/icons/package-zip.svg b/manuscripts/templates/wagtailadmin/icons/package-zip.svg new file mode 100644 index 0000000..bd62e22 --- /dev/null +++ b/manuscripts/templates/wagtailadmin/icons/package-zip.svg @@ -0,0 +1,3 @@ + + + diff --git a/manuscripts/tests/conftest.py b/manuscripts/tests/conftest.py new file mode 100644 index 0000000..e184f0d --- /dev/null +++ b/manuscripts/tests/conftest.py @@ -0,0 +1,61 @@ +import pytest +from django.contrib.auth import get_user_model +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import Client + +from manuscripts.choices import InputType, ProcessStatus +from manuscripts.models.article import Article +from manuscripts.models.processing import Processing + + +@pytest.fixture +def user(db): + return get_user_model().objects.create_user( + username="tester", + password="test-pass-123", + email="tester@example.org", + ) + + +@pytest.fixture +def staff_user(db): + return get_user_model().objects.create_user( + username="staff", + password="test-pass-123", + email="staff@example.org", + is_staff=True, + ) + + +@pytest.fixture +def staff_client(staff_user): + client = Client() + client.force_login(staff_user) + return client + + +@pytest.fixture +def article(db, user): + return Article.objects.create( + title="Happy path article", + doi="10.0000/happy.path", + creator=user, + ) + + +@pytest.fixture +def processing(db, user, tmp_path): + uploaded = SimpleUploadedFile("input.xml", b"
    ", content_type="application/xml") + return Processing.objects.create( + title="Happy path processing", + creator=user, + input_file=uploaded, + ) + + +@pytest.fixture +def xml_processing(processing): + processing.detected_type = InputType.XML + processing.status = ProcessStatus.AWAITING_REVIEW + processing.save(update_fields=["detected_type", "status"]) + return processing diff --git a/manuscripts/tests/test_api.py b/manuscripts/tests/test_api.py new file mode 100644 index 0000000..3441e84 --- /dev/null +++ b/manuscripts/tests/test_api.py @@ -0,0 +1,47 @@ +import pytest +from django.urls import reverse +from rest_framework.test import APIClient + + +@pytest.mark.django_db +def test_first_block_post_returns_deprecated_message(user): + client = APIClient() + client.force_authenticate(user=user) + + response = client.post( + reverse("first_block-list"), + {"text": "sample", "metadata": {"lang": "en"}}, + format="json", + ) + + assert response.status_code == 200 + assert response.json() == {"message": "Article marking API is deprecated."} + + +@pytest.mark.django_db +def test_first_block_post_requires_authentication(): + client = APIClient() + + response = client.post( + reverse("first_block-list"), + {"text": "sample", "metadata": {"lang": "en"}}, + format="json", + ) + + assert response.status_code == 401 + + +@pytest.mark.django_db +def test_first_block_post_with_invalid_json_returns_error_message(user): + client = APIClient() + client.force_authenticate(user=user) + + response = client.generic( + "POST", + reverse("first_block-list"), + "{invalid-json", + content_type="application/json", + ) + + assert response.status_code == 200 + assert response.json() == {"error": "Error processing"} diff --git a/manuscripts/tests/test_apps.py b/manuscripts/tests/test_apps.py new file mode 100644 index 0000000..cfd4c20 --- /dev/null +++ b/manuscripts/tests/test_apps.py @@ -0,0 +1,5 @@ +from manuscripts.apps import ManuscriptsConfig + + +def test_manuscripts_config_name(): + assert ManuscriptsConfig.name == "manuscripts" diff --git a/manuscripts/tests/test_artifacts.py b/manuscripts/tests/test_artifacts.py new file mode 100644 index 0000000..7986417 --- /dev/null +++ b/manuscripts/tests/test_artifacts.py @@ -0,0 +1,449 @@ +import os +import shutil +import socket +import zipfile + +import pytest + +from manuscripts.artifacts import ( + NoRedirectHandler, + article_asset_url_map, + article_assets_dir, + current_xml, + extract_docx_assets, + is_safe_external_url, + resolve_article_assets, + save_artifact, + save_path_artifact, +) +from manuscripts.choices import ArtifactType, EventStatus +from manuscripts.models.article import Article, ArticleArtifact +from manuscripts.models.processing import ProcessingEvent + + +def test_no_redirect_handler_raises_on_redirect(): + handler = NoRedirectHandler() + + with pytest.raises(ValueError, match="Redirecionamentos não são permitidos"): + handler.redirect_request(None, None, 302, "", {}, "https://example.com/new") + + +@pytest.mark.parametrize( + "url", + [ + "ftp://example.com/asset.png", + "http://", + "https:///path", + "not-a-url", + ], +) +def test_is_safe_external_url_rejects_invalid_urls(url): + assert is_safe_external_url(url) is False + + +def test_is_safe_external_url_returns_false_on_dns_failure(monkeypatch): + def raise_gaierror(*_args, **_kwargs): + raise socket.gaierror("name resolution failed") + + monkeypatch.setattr("manuscripts.artifacts.socket.getaddrinfo", raise_gaierror) + + assert is_safe_external_url("https://example.com/asset.png") is False + + +def test_is_safe_external_url_rejects_non_global_addresses(monkeypatch): + monkeypatch.setattr( + "manuscripts.artifacts.socket.getaddrinfo", + lambda *_args, **_kwargs: [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("127.0.0.1", 443))], + ) + + assert is_safe_external_url("https://example.com/asset.png") is False + + +def test_is_safe_external_url_accepts_global_addresses(monkeypatch): + monkeypatch.setattr( + "manuscripts.artifacts.socket.getaddrinfo", + lambda *_args, **_kwargs: [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("8.8.8.8", 443))], + ) + + assert is_safe_external_url("https://example.com/asset.png") is True + + +def test_is_safe_external_url_uses_explicit_port(monkeypatch): + captured = {} + + def capture_getaddrinfo(host, port, *_args, **_kwargs): + captured["host"] = host + captured["port"] = port + return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("8.8.8.8", port))] + + monkeypatch.setattr("manuscripts.artifacts.socket.getaddrinfo", capture_getaddrinfo) + + assert is_safe_external_url("http://cdn.example.com:8080/image.png") is True + assert captured == {"host": "cdn.example.com", "port": 8080} + + +@pytest.mark.django_db +def test_save_artifact_asset_uses_basename_when_original_path_missing(processing, article): + artifact = save_artifact( + processing, + ArtifactType.ASSET, + "nested/path/figure.png", + b"png-bytes", + article=article, + ) + + assert artifact.original_path == "figure.png" + assert artifact.file.name.endswith("figure.png") + + +@pytest.mark.django_db +def test_save_artifact_asset_versions_by_original_path(processing, article): + first = save_artifact( + processing, + ArtifactType.ASSET, + "a.png", + b"first", + article=article, + original_path="media/a.png", + ) + second = save_artifact( + processing, + ArtifactType.ASSET, + "b.png", + b"second", + article=article, + original_path="media/b.png", + ) + updated = save_artifact( + processing, + ArtifactType.ASSET, + "a.png", + b"updated", + article=article, + original_path="media/a.png", + ) + + first.refresh_from_db() + second.refresh_from_db() + updated.refresh_from_db() + assert first.version == 1 + assert second.version == 1 + assert updated.version == 2 + assert first.is_current is False + assert second.is_current is True + assert updated.is_current is True + + +@pytest.mark.django_db +def test_save_path_artifact_reads_file_content(processing, article, tmp_path): + source = tmp_path / "article.xml" + source.write_bytes(b"
    ") + + artifact = save_path_artifact( + processing, + ArtifactType.XML, + str(source), + article=article, + metadata={"source": "disk"}, + ) + + assert artifact.artifact_type == ArtifactType.XML + assert artifact.metadata == {"source": "disk"} + with artifact.file.open("rb") as stored: + assert stored.read() == b"
    " + + +@pytest.mark.django_db +def test_current_xml_returns_current_artifact(processing, article): + xml_artifact = save_artifact( + processing, + ArtifactType.XML, + "article.xml", + b"
    ", + article=article, + ) + + assert current_xml(processing, article) == xml_artifact + + +@pytest.mark.django_db +def test_current_xml_raises_when_missing(processing, article): + with pytest.raises(ValueError, match=f"Artigo {article} não possui XML"): + current_xml(processing, article) + + +@pytest.mark.django_db +def test_article_asset_url_map_uses_original_path_and_file_name(processing, article): + with_path = save_artifact( + processing, + ArtifactType.ASSET, + "stored-a.png", + b"a", + article=article, + original_path="word/media/a.png", + ) + without_path = save_artifact( + processing, + ArtifactType.ASSET, + "stored-b.png", + b"b", + article=article, + ) + + url_map = article_asset_url_map(processing, article) + + assert url_map == { + "a.png": with_path.file.url, + "stored-b.png": without_path.file.url, + } + + +@pytest.mark.django_db +def test_article_assets_dir_returns_none_when_no_assets(processing, article): + assert article_assets_dir(processing, article) is None + + +@pytest.mark.django_db +def test_article_assets_dir_creates_symlinks(processing, article): + save_artifact( + processing, + ArtifactType.ASSET, + "figure.png", + b"png", + article=article, + original_path="word/media/figure.png", + ) + + assets_dir = article_assets_dir(processing, article) + try: + target = os.path.join(assets_dir, "figure.png") + assert os.path.islink(target) + with open(target, "rb") as linked: + assert linked.read() == b"png" + finally: + shutil_rmtree(assets_dir) + + +@pytest.mark.django_db +def test_article_assets_dir_falls_back_to_copy_when_symlink_fails(processing, article, monkeypatch): + save_artifact( + processing, + ArtifactType.ASSET, + "figure.png", + b"png", + article=article, + original_path="word/media/figure.png", + ) + + def fail_symlink(*_args, **_kwargs): + raise OSError("symlinks not supported") + + monkeypatch.setattr("manuscripts.artifacts.os.symlink", fail_symlink) + + assets_dir = article_assets_dir(processing, article) + try: + target = os.path.join(assets_dir, "figure.png") + assert os.path.isfile(target) + assert not os.path.islink(target) + with open(target, "rb") as copied: + assert copied.read() == b"png" + finally: + shutil_rmtree(assets_dir) + + +@pytest.mark.django_db +def test_resolve_article_assets_reassigns_local_asset_for_same_article(processing, article): + local = save_artifact( + processing, + ArtifactType.ASSET, + "figure.png", + b"png", + article=article, + original_path="figure.png", + ) + + resolve_article_assets(processing, article, ["figure.png"]) + + local.refresh_from_db() + assert local.article_id == article.pk + assert ArticleArtifact.objects.filter( + processing=processing, + article=article, + artifact_type=ArtifactType.ASSET, + original_path="figure.png", + is_current=True, + ).count() == 1 + + +@pytest.mark.django_db +def test_resolve_article_assets_copies_local_asset_from_other_article(processing, article, user): + other_article = Article.objects.create(title="Other", doi="10.0000/other", creator=user) + shared = save_artifact( + processing, + ArtifactType.ASSET, + "shared.png", + b"shared", + article=other_article, + original_path="shared.png", + source_url="https://example.com/shared.png", + ) + + resolve_article_assets(processing, article, ["shared.png"]) + + copied = ArticleArtifact.objects.get( + processing=processing, + article=article, + artifact_type=ArtifactType.ASSET, + original_path="shared.png", + is_current=True, + ) + shared.refresh_from_db() + assert copied.pk != shared.pk + assert copied.source_url == "https://example.com/shared.png" + with copied.file.open("rb") as stored: + assert stored.read() == b"shared" + + +@pytest.mark.django_db +def test_resolve_article_assets_records_blocked_external_url(processing, article): + resolve_article_assets(processing, article, ["file:///etc/passwd"]) + + event = ProcessingEvent.objects.get(processing=processing, article=article) + assert event.status == EventStatus.FAILED + assert "bloqueada" in event.message + assert event.details["asset"] == "file:///etc/passwd" + + +@pytest.mark.django_db +def test_resolve_article_assets_downloads_safe_external_asset(processing, article, monkeypatch): + monkeypatch.setattr("manuscripts.artifacts.is_safe_external_url", lambda _url: True) + monkeypatch.setattr("manuscripts.artifacts.build_opener", mock_external_opener(b"remote-png")) + + resolve_article_assets( + processing, + article, + ["https://cdn.example.com/assets/remote.png?version=1"], + ) + + artifact = ArticleArtifact.objects.get( + processing=processing, + article=article, + artifact_type=ArtifactType.ASSET, + original_path="remote.png", + is_current=True, + ) + assert artifact.source_url == "https://cdn.example.com/assets/remote.png?version=1" + with artifact.file.open("rb") as stored: + assert stored.read() == b"remote-png" + + +@pytest.mark.django_db +def test_resolve_article_assets_skips_downloads_larger_than_25mb(processing, article, monkeypatch): + monkeypatch.setattr("manuscripts.artifacts.is_safe_external_url", lambda _url: True) + oversized = b"x" * (25 * 1024 * 1024 + 1) + monkeypatch.setattr("manuscripts.artifacts.build_opener", mock_external_opener(oversized)) + + resolve_article_assets(processing, article, ["https://cdn.example.com/huge.bin"]) + + assert not ArticleArtifact.objects.filter( + processing=processing, + article=article, + artifact_type=ArtifactType.ASSET, + ).exists() + assert not ProcessingEvent.objects.filter(processing=processing, article=article).exists() + + +@pytest.mark.django_db +def test_resolve_article_assets_records_download_exception(processing, article, monkeypatch): + monkeypatch.setattr("manuscripts.artifacts.is_safe_external_url", lambda _url: True) + + class BrokenOpener: + def open(self, *_args, **_kwargs): + raise OSError("connection reset") + + monkeypatch.setattr( + "manuscripts.artifacts.build_opener", + lambda _handler: BrokenOpener(), + ) + + resolve_article_assets(processing, article, ["https://cdn.example.com/missing.png"]) + + event = ProcessingEvent.objects.get(processing=processing, article=article) + assert event.status == EventStatus.FAILED + assert "Não foi possível baixar o asset" in event.message + assert event.details["error"] == "connection reset" + + +@pytest.mark.django_db +def test_extract_docx_assets_saves_word_media_members(processing, article, tmp_path): + docx_path = tmp_path / "article.docx" + with zipfile.ZipFile(docx_path, "w") as archive: + archive.writestr("word/media/figure1.png", b"png-one") + archive.writestr("word/media/figure2.jpg", b"jpg-two") + archive.writestr("word/document.xml", b"") + archive.writestr("../escape.png", b"ignored") + + source = save_path_artifact( + processing, + ArtifactType.SOURCE_DOCUMENT, + str(docx_path), + article=article, + ) + + extract_docx_assets(processing, source, article) + + saved = { + artifact.original_path: artifact.file.read() + for artifact in ArticleArtifact.objects.filter( + processing=processing, + article=article, + artifact_type=ArtifactType.ASSET, + is_current=True, + ) + } + assert saved == { + "figure1.png": b"png-one", + "figure2.jpg": b"jpg-two", + } + + +@pytest.mark.django_db +def test_extract_docx_assets_ignores_bad_zip_files(processing, article, tmp_path): + broken = tmp_path / "broken.docx" + broken.write_bytes(b"not-a-zip") + source = save_path_artifact( + processing, + ArtifactType.SOURCE_DOCUMENT, + str(broken), + article=article, + ) + + extract_docx_assets(processing, source, article) + + assert not ArticleArtifact.objects.filter( + processing=processing, + article=article, + artifact_type=ArtifactType.ASSET, + ).exists() + + +def mock_external_opener(content): + class MockResponse: + def read(self, _max_bytes): + return content + + def __enter__(self): + return self + + def __exit__(self, *_args): + return False + + class MockOpener: + def open(self, _request, timeout=10): + return MockResponse() + + return lambda _handler: MockOpener() + + +def shutil_rmtree(path): + shutil.rmtree(path, ignore_errors=True) diff --git a/manuscripts/tests/test_autocomplete.py b/manuscripts/tests/test_autocomplete.py new file mode 100644 index 0000000..4f589c6 --- /dev/null +++ b/manuscripts/tests/test_autocomplete.py @@ -0,0 +1,126 @@ +import json +from http import HTTPStatus +from unittest.mock import MagicMock + +import pytest +from django.test import RequestFactory +from journals.models import Issue, Journal + +from manuscripts.views.autocomplete import search + + +@pytest.fixture +def request_factory(): + return RequestFactory() + + +def _post_search(request_factory, staff_user, data): + request = request_factory.post("/admin/autocomplete/search/", data) + request.user = staff_user + return search(request) + + +@pytest.fixture +def journal_with_issues(db): + journal = Journal.objects.create(title="Test Journal") + issue_one = Issue.objects.create(journal=journal, volume="1", number="1", year="2024") + issue_two = Issue.objects.create(journal=journal, volume="2", number="3", year="2025") + return journal, issue_one, issue_two + + +@pytest.mark.django_db +def test_search_delegates_to_default_search(request_factory, staff_user, monkeypatch): + delegated = MagicMock(return_value=MagicMock(status_code=HTTPStatus.OK)) + monkeypatch.setattr("manuscripts.views.autocomplete.default_search", delegated) + + response = _post_search( + request_factory, + staff_user, + {"type": "wagtailcore.Page", "query": "home"}, + ) + + delegated.assert_called_once() + assert response.status_code == HTTPStatus.OK + + +@pytest.mark.django_db +def test_search_delegates_when_issue_filter_flag_missing(request_factory, staff_user, monkeypatch): + delegated = MagicMock(return_value=MagicMock(status_code=HTTPStatus.OK)) + monkeypatch.setattr("manuscripts.views.autocomplete.default_search", delegated) + + _post_search( + request_factory, + staff_user, + {"type": "journals.Issue", "journal_id": "1"}, + ) + + delegated.assert_called_once() + + +@pytest.mark.django_db +def test_search_returns_empty_items_without_journal_id(request_factory, staff_user): + response = _post_search( + request_factory, + staff_user, + {"type": "journals.Issue", "article_issue_filter": "1"}, + ) + + assert response.status_code == HTTPStatus.OK + payload = json.loads(response.content) + assert payload == {"items": []} + + +@pytest.mark.django_db +def test_search_filters_issues_by_journal_query_and_exclude(request_factory, staff_user, journal_with_issues): + journal, issue_one, issue_two = journal_with_issues + + response = _post_search( + request_factory, + staff_user, + { + "type": "journals.Issue", + "article_issue_filter": "1", + "journal_id": str(journal.pk), + "query": "2024", + "exclude": str(issue_two.pk), + "limit": "10", + }, + ) + + assert response.status_code == HTTPStatus.OK + items = json.loads(response.content)["items"] + assert len(items) == 1 + assert items[0]["pk"] == issue_one.pk + + +@pytest.mark.django_db +def test_search_returns_bad_request_for_invalid_model(request_factory, staff_user): + response = _post_search( + request_factory, + staff_user, + { + "type": "not.a.RealModel", + "article_issue_filter": "1", + "journal_id": "1", + }, + ) + + assert response.status_code == HTTPStatus.BAD_REQUEST + + +@pytest.mark.django_db +def test_search_returns_bad_request_for_invalid_limit(request_factory, staff_user, journal_with_issues): + journal, _issue_one, _issue_two = journal_with_issues + + response = _post_search( + request_factory, + staff_user, + { + "type": "journals.Issue", + "article_issue_filter": "1", + "journal_id": str(journal.pk), + "limit": "not-a-number", + }, + ) + + assert response.status_code == HTTPStatus.BAD_REQUEST diff --git a/manuscripts/tests/test_controller.py b/manuscripts/tests/test_controller.py new file mode 100644 index 0000000..f122969 --- /dev/null +++ b/manuscripts/tests/test_controller.py @@ -0,0 +1,395 @@ +from datetime import date + +import pytest + +from journals.models import Issue, Journal +from manuscripts.choices import EventStatus, ProcessingAction +from manuscripts.controller import ( + enrich_article, + event_complete, + event_details_update, + event_start, + get_or_create_article, +) +from manuscripts.models.article import Article +from manuscripts.utils.helpers import checksum_bytes + + +@pytest.mark.django_db +def test_get_or_create_article_creates_new_article(processing, monkeypatch): + metadata = { + "title": "Created title", + "doi": "10.1234/created", + "pid": "S00001", + "language": "pt", + "license": "https://creativecommons.org/licenses/by/4.0/", + "elocatid": "e123", + "fpage": "1", + "lpage": "10", + "seq": "1", + "artdate": "2021-05-20", + } + xml_content = b"
    " + monkeypatch.setattr("manuscripts.controller.extract_article_metadata", lambda _xml: metadata) + + article, returned_metadata = get_or_create_article( + processing=processing, + xml_content=xml_content, + ) + + assert article.pk is not None + assert article.title == "Created title" + assert article.doi == "10.1234/created" + assert article.pid == "S00001" + assert article.language == "pt" + assert article.license == metadata["license"] + assert article.elocatid == "e123" + assert article.fpage == "1" + assert article.lpage == "10" + assert article.seq == "1" + assert article.artdate == date(2021, 5, 20) + assert article.content_checksum == checksum_bytes(xml_content) + assert processing.articles.filter(pk=article.pk).exists() + assert returned_metadata == metadata + + +@pytest.mark.django_db +def test_get_or_create_article_without_xml_uses_processing_checksum_and_title(processing): + article, metadata = get_or_create_article(processing=processing, title="Fallback title") + + assert article.title == "Fallback title" + assert article.content_checksum == processing.input_checksum + assert metadata == {} + assert processing.articles.filter(pk=article.pk).exists() + + +@pytest.mark.django_db +def test_get_or_create_article_matches_existing_by_pid(processing, user, monkeypatch): + existing = Article.objects.create(pid="S99999", title="Existing", creator=user) + monkeypatch.setattr( + "manuscripts.controller.extract_article_metadata", + lambda _xml: {"pid": "S99999", "title": "Should not replace"}, + ) + + article, _metadata = get_or_create_article(processing=processing, xml_content=b"
    ") + + assert article.pk == existing.pk + assert article.title == "Existing" + assert Article.objects.count() == 1 + + +@pytest.mark.django_db +def test_get_or_create_article_matches_existing_by_checksum(processing, user, monkeypatch): + xml_content = b"
    " + checksum = checksum_bytes(xml_content) + existing = Article.objects.create(content_checksum=checksum, title="Checksum match", creator=user) + monkeypatch.setattr( + "manuscripts.controller.extract_article_metadata", + lambda _xml: {"title": "Ignored title"}, + ) + + article, _metadata = get_or_create_article(processing=processing, xml_content=xml_content) + + assert article.pk == existing.pk + assert article.title == "Checksum match" + assert Article.objects.count() == 1 + + +@pytest.mark.django_db +def test_get_or_create_article_updates_empty_fields_on_existing_article(processing, user, monkeypatch): + existing = Article.objects.create( + doi="10.1234/update-me", + title="", + pid="", + language="", + license=None, + elocatid="", + fpage="", + lpage="", + seq="", + artdate=None, + creator=user, + ) + monkeypatch.setattr( + "manuscripts.controller.extract_article_metadata", + lambda _xml: { + "doi": "10.1234/update-me", + "title": "Filled title", + "pid": "S12345", + "language": "en", + "license": "https://example.org/license", + "elocatid": "e42", + "fpage": "3", + "lpage": "7", + "seq": "2", + "artdate": "2022-01-15", + }, + ) + + article, _metadata = get_or_create_article(processing=processing, xml_content=b"
    ") + + article.refresh_from_db() + assert article.pk == existing.pk + assert article.title == "Filled title" + assert article.pid == "S12345" + assert article.language == "en" + assert article.license == "https://example.org/license" + assert article.elocatid == "e42" + assert article.fpage == "3" + assert article.lpage == "7" + assert article.seq == "2" + assert article.artdate == date(2022, 1, 15) + + +@pytest.mark.django_db +def test_get_or_create_article_does_not_overwrite_filled_fields(processing, user, monkeypatch): + existing = Article.objects.create( + doi="10.1234/keep", + title="Keep title", + pid="KEEP-PID", + language="pt", + license="https://example.org/existing", + elocatid="keep-eloc", + fpage="10", + lpage="20", + seq="9", + artdate=date(2019, 6, 1), + creator=user, + ) + monkeypatch.setattr( + "manuscripts.controller.extract_article_metadata", + lambda _xml: { + "doi": "10.1234/keep", + "title": "New title", + "pid": "NEW-PID", + "language": "en", + "license": "https://example.org/new", + "elocatid": "new-eloc", + "fpage": "1", + "lpage": "2", + "seq": "1", + "artdate": "2024-01-01", + }, + ) + + article, _metadata = get_or_create_article(processing=processing, xml_content=b"
    ") + + article.refresh_from_db() + assert article.pk == existing.pk + assert article.title == "Keep title" + assert article.pid == "KEEP-PID" + assert article.language == "pt" + assert article.license == "https://example.org/existing" + assert article.elocatid == "keep-eloc" + assert article.fpage == "10" + assert article.lpage == "20" + assert article.seq == "9" + assert article.artdate == date(2019, 6, 1) + + +@pytest.mark.django_db +def test_enrich_article_updates_doi_title_and_dates(user): + article = Article.objects.create( + title="Old title", + doi="10.1234/old", + creator=user, + ) + + updates = enrich_article( + { + "doi": "10.1234/new", + "titles": [{"text": "New title"}], + "dates": [ + {"type": "published", "date": "2021-03-01"}, + {"type": "ahp", "date": "2020-12-15"}, + {"type": "published", "date": "invalid"}, + ], + }, + article, + ) + + assert updates == { + "doi": "10.1234/new", + "title": "New title", + "artdate": date(2021, 3, 1), + "ahpdate": date(2020, 12, 15), + } + + +@pytest.mark.django_db +def test_enrich_article_skips_unchanged_doi_title_and_dates(user): + article = Article.objects.create( + title="Same title", + doi="10.1234/same", + artdate=date(2021, 3, 1), + ahpdate=date(2020, 12, 15), + creator=user, + ) + + updates = enrich_article( + { + "doi": "10.1234/same", + "titles": [{"text": "Same title"}], + "dates": [ + {"type": "published", "date": "2021-03-01"}, + {"type": "ahp", "date": "2020-12-15"}, + ], + }, + article, + ) + + assert updates == {} + + +@pytest.mark.django_db +def test_enrich_article_matches_journal_by_issn(user): + journal = Journal.objects.create(title="ISSN Journal", issn="1234-5678") + article = Article.objects.create(creator=user) + + updates = enrich_article({"journal": {"issn": "1234-5678"}}, article) + + assert updates == {"journal": journal} + + +@pytest.mark.django_db +def test_enrich_article_matches_journal_by_title_when_issn_missing(user): + journal = Journal.objects.create(title="Revista Example", short_title="RE") + article = Article.objects.create(creator=user) + + updates = enrich_article({"journal": {"title": "Revista Example"}}, article) + + assert updates == {"journal": journal} + + +@pytest.mark.django_db +def test_enrich_article_matches_journal_by_issn_without_dashes(user): + journal = Journal.objects.create(title="Dashless ISSN", eissn="98765432") + article = Article.objects.create(creator=user) + + updates = enrich_article({"journal": {"issn": "9876-5432"}}, article) + + assert updates == {"journal": journal} + + +@pytest.mark.django_db +def test_enrich_article_matches_issue_for_journal(user): + journal = Journal.objects.create(title="Issue Journal", issn="1111-2222") + issue = Issue.objects.create( + journal=journal, + volume="10", + number="2", + year="2023", + supplement="S1", + ) + article = Article.objects.create(creator=user) + + updates = enrich_article( + { + "journal": {"issn": "1111-2222"}, + "issue": { + "volume": "10", + "number": "2", + "year": "2023", + "supplement": "S1", + }, + }, + article, + ) + + assert updates["journal"] == journal + assert updates["issue"] == issue + + +@pytest.mark.django_db +def test_enrich_article_uses_existing_journal_and_skips_issue_when_already_set(user): + journal = Journal.objects.create(title="Linked Journal") + issue = Issue.objects.create(journal=journal, volume="1", number="1", year="2020") + article = Article.objects.create(journal=journal, issue=issue, creator=user) + + updates = enrich_article( + { + "journal": {"title": "Other Journal"}, + "issue": {"volume": "99", "number": "99", "year": "2099"}, + }, + article, + ) + + assert updates == {} + + +@pytest.mark.django_db +def test_enrich_article_does_not_set_issue_when_no_match(user): + journal = Journal.objects.create(title="No Issue Journal") + article = Article.objects.create(creator=user) + + updates = enrich_article( + { + "journal": {"title": "No Issue Journal"}, + "issue": {"volume": "404", "number": "404", "year": "2099"}, + }, + article, + ) + + assert updates == {"journal": journal} + + +@pytest.mark.django_db +def test_event_details_update_merges_details(processing): + event = event_start( + processing=processing, + action=ProcessingAction.XML_VALIDATION, + task_id="task-merge", + ) + event.details = {"step": 1, "keep": True} + event.save(update_fields=["details"]) + + event_details_update(event, {"step": 2, "extra": "value"}) + + event.refresh_from_db() + assert event.details == {"step": 2, "keep": True, "extra": "value"} + + +@pytest.mark.django_db +def test_event_details_update_handles_none_details_argument(processing): + event = event_start( + processing=processing, + action=ProcessingAction.XML_VALIDATION, + task_id="task-empty", + article=None, + ) + + event_details_update(event, None) + + event.refresh_from_db() + assert event.details == {} + + +@pytest.mark.django_db +def test_event_complete_sets_status_message_and_details(processing): + event = event_start( + processing=processing, + action=ProcessingAction.XML_VALIDATION, + task_id="task-complete", + ) + + event_complete(event, message="finished", details={"count": 3}, status=EventStatus.FAILED) + + event.refresh_from_db() + assert event.status == EventStatus.FAILED + assert event.message == "finished" + assert event.details == {"count": 3} + assert event.completed_at is not None + + +@pytest.mark.django_db +def test_event_start_links_article(processing, article): + event = event_start( + processing=processing, + action=ProcessingAction.XML_GENERATION, + task_id="", + article=article, + ) + + assert event.article == article + assert event.task_id == "" + assert event.status == EventStatus.RUNNING diff --git a/manuscripts/tests/test_docx_utils.py b/manuscripts/tests/test_docx_utils.py new file mode 100644 index 0000000..cec9e32 --- /dev/null +++ b/manuscripts/tests/test_docx_utils.py @@ -0,0 +1,235 @@ +import pytest + +from manuscripts.utils.docx_utils import extract_docx_structure + + +class _ImageWithPk: + pk = 42 + + +def _labeled_block(label, paragraph, section="front"): + return ( + {"type": "paragraph", "value": {"label": label, "paragraph": paragraph}}, + {"label": label, "body": section == "body", "back": section == "back"}, + {"label": label, "body": section == "body", "back": section == "back"}, + ) + + +@pytest.fixture +def docx_mocks(monkeypatch): + sections = [{"name": "body"}] + content = [ + {"type": "first_block", "text": " Article headline "}, + {"type": "first_block", "text": " "}, + {"type": "image", "image": _ImageWithPk()}, + {"type": "table", "table": ""}, + {"type": "list", "list": "item one"}, + {"type": "compound", "text": "formula"}, + {"type": "paragraph", "text": " "}, + {"type": "paragraph", "text": "Front section note"}, + {"type": "paragraph", "text": "Body paragraph cites Smith 2020 here."}, + {"type": "paragraph", "text": "Back matter note"}, + {"type": "paragraph", "text": "Reference one"}, + {"type": "paragraph", "text": "Reference two"}, + ] + + class FakeParser: + def extract_content(self, document, source_path, merge_front=False): + return sections, content + + labeled_calls = [] + + def fake_create_labeled_object(index, item, state, section_list): + labeled_calls.append(item) + text = item.get("text") or "" + if not text.strip(): + return None, None, state + if text.startswith("Body paragraph"): + return _labeled_block("

    ", text, "body") + if text == "Front section note": + return _labeled_block("

    ", text, "front") + if text == "Back matter note": + return _labeled_block("", text, "back") + if text.startswith("Reference"): + state = {"label": "

    ", "body": False, "back": True} + return _labeled_block("

    ", text, "back") + return None, None, state + + special_calls = [] + + def fake_create_special_content_object(item, body, counts): + special_calls.append((item, list(body))) + item_type = item.get("type") + if item_type == "image": + return { + "type": "image", + "value": {"label": "", "image": item.get("image"), "title": "", "content": ""}, + }, counts + if item_type == "table": + return { + "type": "table", + "value": {"label": "

    ", "title": "Affiliation", "content": "University"}, + }, counts + if item_type == "list": + return {"type": "paragraph", "value": {"label": "", "paragraph": item.get("list")}}, counts + if item_type == "compound": + return {"type": "compound_paragraph", "value": {"label": "", "content": item.get("text")}}, counts + return None, counts + + frontmatter_texts = [] + + def fake_looks_like_frontmatter(text): + frontmatter_texts.append(text) + return "Affiliation" in text + + replacer_calls = [] + + def fake_replacer(text): + replacer_calls.append(text) + return text + " [replaced]" + + monkeypatch.setattr("manuscripts.utils.docx_utils.DocxParser", FakeParser) + monkeypatch.setattr("manuscripts.utils.docx_utils.create_labeled_object", fake_create_labeled_object) + monkeypatch.setattr("manuscripts.utils.docx_utils.create_special_content_object", fake_create_special_content_object) + monkeypatch.setattr("manuscripts.utils.docx_utils._looks_like_frontmatter", fake_looks_like_frontmatter) + monkeypatch.setattr( + "manuscripts.utils.docx_utils.read_marks", + lambda document: [ + {"rid": "B1", "citations": ["Smith 2020", ""]}, + {"rid": "B2", "citations": ["Smith"]}, + ], + ) + monkeypatch.setattr("manuscripts.utils.docx_utils.build_text_xref_replacer", lambda document: fake_replacer) + monkeypatch.setattr("manuscripts.utils.docx_utils.validate_marks", lambda document: {"valid": True}) + + return { + "labeled_calls": labeled_calls, + "special_calls": special_calls, + "frontmatter_texts": frontmatter_texts, + "replacer_calls": replacer_calls, + } + + +@pytest.mark.django_db +def test_extract_docx_structure_routes_content_and_applies_xrefs(article, docx_mocks): + document = object() + front, body, back, xref_status = extract_docx_structure(document, "/tmp/sample.docx") + + assert front[0]["value"]["label"] == "" + assert front[0]["value"]["paragraph"] == " Article headline " + assert any(item["type"] == "table" for item in front) + assert any(item["type"] == "image" and item["value"]["image"] == 42 for item in body) + assert any(item["value"]["label"] == "" for item in body) + assert any(item["value"]["label"] == "" for item in body) + assert any(item["value"]["label"] == "

    " and item["value"]["paragraph"].endswith("[replaced]") for item in body) + paragraph = next(item for item in body if item["value"].get("label") == "

    ") + assert 'rid="B1"' in paragraph["value"]["paragraph"] + assert 'rid="B2"' in paragraph["value"]["paragraph"] + assert paragraph["value"]["paragraph"].endswith("[replaced]") + assert any(item.get("value", {}).get("paragraph") == "Front section note" for item in front) + assert back[0]["value"]["label"] == "" + assert back[1]["type"] == "ref_paragraph" + assert back[1]["value"]["refid"] == "B1" + assert back[2]["value"]["refid"] == "B2" + assert xref_status == {"valid": True} + assert docx_mocks["replacer_calls"] + + +def test_extract_docx_structure_empty_body_fallback(monkeypatch): + sections = [] + content = [ + {"type": "first_block", "text": ""}, + {"type": "paragraph", "text": "Only front paragraph"}, + ] + + class FakeParser: + def extract_content(self, document, source_path, merge_front=False): + return sections, content + + monkeypatch.setattr("manuscripts.utils.docx_utils.DocxParser", FakeParser) + monkeypatch.setattr("manuscripts.utils.docx_utils.create_labeled_object", lambda *args: (None, None, args[2])) + monkeypatch.setattr("manuscripts.utils.docx_utils.create_special_content_object", lambda *args: (None, args[2])) + monkeypatch.setattr("manuscripts.utils.docx_utils.read_marks", lambda document: []) + monkeypatch.setattr("manuscripts.utils.docx_utils.build_text_xref_replacer", lambda document: lambda text: text) + monkeypatch.setattr("manuscripts.utils.docx_utils.validate_marks", lambda document: []) + + front, body, back, xref_status = extract_docx_structure(object(), "draft.docx") + + assert front == [] + assert body == [{"type": "paragraph", "value": {"label": "

    ", "paragraph": "Only front paragraph"}}] + assert back == [] + assert xref_status == [] + + +def test_extract_docx_structure_skips_none_special_object(monkeypatch): + sections = [] + content = [{"type": "image", "image": None}] + + class FakeParser: + def extract_content(self, document, source_path, merge_front=False): + return sections, content + + monkeypatch.setattr("manuscripts.utils.docx_utils.DocxParser", FakeParser) + monkeypatch.setattr( + "manuscripts.utils.docx_utils.create_special_content_object", + lambda item, body, counts: (None, counts), + ) + monkeypatch.setattr("manuscripts.utils.docx_utils.read_marks", lambda document: []) + monkeypatch.setattr("manuscripts.utils.docx_utils.build_text_xref_replacer", lambda document: lambda text: text) + monkeypatch.setattr("manuscripts.utils.docx_utils.validate_marks", lambda document: None) + + front, body, back, xref_status = extract_docx_structure(object(), "empty.docx") + + assert front == [] + assert body == [] + assert back == [] + assert xref_status is None + + +def test_extract_docx_structure_xref_skips_non_paragraph_and_existing_markup(monkeypatch): + sections = [] + content = [{"type": "paragraph", "text": "Already linked"}] + + class FakeParser: + def extract_content(self, document, source_path, merge_front=False): + return sections, content + + def fake_create_labeled_object(index, item, state, section_list): + return ( + {"type": "paragraph", "value": {"label": "", "paragraph": item["text"]}}, + {}, + state, + ) + + monkeypatch.setattr("manuscripts.utils.docx_utils.DocxParser", FakeParser) + monkeypatch.setattr("manuscripts.utils.docx_utils.create_labeled_object", fake_create_labeled_object) + monkeypatch.setattr("manuscripts.utils.docx_utils.create_special_content_object", lambda *args: (None, args[2])) + monkeypatch.setattr("manuscripts.utils.docx_utils._looks_like_frontmatter", lambda text: False) + monkeypatch.setattr( + "manuscripts.utils.docx_utils.read_marks", + lambda document: [{"rid": "B9", "citations": ["linked"]}], + ) + monkeypatch.setattr("manuscripts.utils.docx_utils.build_text_xref_replacer", lambda document: lambda text: text) + monkeypatch.setattr("manuscripts.utils.docx_utils.validate_marks", lambda document: "ok") + + body_item = { + "type": "paragraph", + "value": { + "label": "

    ", + "paragraph": 'See linked citation.', + }, + } + + monkeypatch.setattr( + "manuscripts.utils.docx_utils.create_labeled_object", + lambda index, item, state, section_list: ( + body_item, + {}, + {"body": True, "back": False}, + ), + ) + + _, body, _, _ = extract_docx_structure(object(), "xref.docx") + + assert ">linked" in body[0]["value"]["paragraph"] + assert body[0]["value"]["paragraph"].count("", + article=article, + structure=structure, + ) + ProcessingEvent.objects.create(processing=processing, article=article, creator=user) + request = _staff_request( + request_factory, + staff_user, + "post", + reverse("manuscripts:processing_cleanup_artifacts", kwargs={"pk": processing.pk}), + {"confirmation": "LIMPAR"}, + ) + + response = editorial_views.processing_cleanup_artifacts(request, processing.pk) + + processing.refresh_from_db() + article.refresh_from_db() + assert response.status_code == 302 + assert not ArticleArtifact.objects.filter(pk=artifact.pk).exists() + assert not ArticleStructureVersion.objects.filter(pk=structure.pk).exists() + assert processing.status == ProcessStatus.AWAITING_REVIEW + assert article.status == ProcessStatus.PENDING + + +@pytest.mark.django_db +def test_processing_cleanup_artifacts_include_input_cancels_processing( + request_factory, staff_user, processing, article +): + processing.articles.add(article) + request = _staff_request( + request_factory, + staff_user, + "post", + reverse("manuscripts:processing_cleanup_artifacts", kwargs={"pk": processing.pk}), + {"confirmation": "LIMPAR", "include_input": "1"}, + ) + + response = editorial_views.processing_cleanup_artifacts(request, processing.pk) + + processing.refresh_from_db() + assert response.status_code == 302 + assert processing.status == ProcessStatus.CANCELLED + assert not processing.input_file.storage.exists(processing.input_file.name) + + +@pytest.mark.django_db +def test_processing_cleanup_artifacts_sets_article_pending_without_structure( + request_factory, staff_user, processing, article +): + processing.articles.add(article) + article.status = ProcessStatus.COMPLETED + article.save(update_fields=["status"]) + request = _staff_request( + request_factory, + staff_user, + "post", + reverse("manuscripts:processing_cleanup_artifacts", kwargs={"pk": processing.pk}), + {"confirmation": "LIMPAR"}, + ) + + editorial_views.processing_cleanup_artifacts(request, processing.pk) + + article.refresh_from_db() + assert article.status == ProcessStatus.PENDING + + +@pytest.mark.django_db +def test_processing_cleanup_artifacts_restores_latest_structure_version( + request_factory, staff_user, processing, article, user +): + other_processing = Processing.objects.create( + title="Other processing", + creator=user, + input_file=SimpleUploadedFile("other.xml", b"

    ", content_type="application/xml"), + ) + processing.articles.add(article) + older = ArticleStructureVersion.objects.create( + article=article, + processing=other_processing, + version=1, + is_current=False, + source_kind=InputType.XML, + front=[], + body=[], + back=[], + creator=user, + ) + ArticleStructureVersion.objects.create( + article=article, + processing=processing, + version=2, + is_current=True, + source_kind=InputType.XML, + front=[], + body=[], + back=[], + creator=user, + ) + request = _staff_request( + request_factory, + staff_user, + "post", + reverse("manuscripts:processing_cleanup_artifacts", kwargs={"pk": processing.pk}), + {"confirmation": "LIMPAR"}, + ) + + editorial_views.processing_cleanup_artifacts(request, processing.pk) + + older.refresh_from_db() + assert older.is_current is True + + +@pytest.mark.django_db +def test_article_cleanup_artifacts_requires_confirmation(request_factory, staff_user, article): + request = _staff_request( + request_factory, + staff_user, + "post", + reverse("manuscripts:article_cleanup_artifacts", kwargs={"pk": article.pk}), + {}, + ) + + response = editorial_views.article_cleanup_artifacts(request, article.pk) + + assert response.status_code == 302 + messages = [message.message for message in get_messages(request)] + assert any("LIMPAR" in message for message in messages) + + +@pytest.mark.django_db +def test_article_cleanup_artifacts_deletes_all_related_records( + request_factory, staff_user, processing, article, user +): + structure = create_structure_version(article, processing, InputType.XML, [], [], []) + artifact = save_artifact( + processing, + ArtifactType.XML, + "article.xml", + b"
    ", + article=article, + structure=structure, + ) + ProcessingEvent.objects.create(processing=processing, article=article, creator=user) + request = _staff_request( + request_factory, + staff_user, + "post", + reverse("manuscripts:article_cleanup_artifacts", kwargs={"pk": article.pk}), + {"confirmation": "LIMPAR"}, + ) + + response = editorial_views.article_cleanup_artifacts(request, article.pk) + + article.refresh_from_db() + assert response.status_code == 302 + assert not ArticleArtifact.objects.filter(pk=artifact.pk).exists() + assert not ArticleStructureVersion.objects.filter(pk=structure.pk).exists() + assert article.status == ProcessStatus.PENDING + + +@pytest.mark.django_db +def test_artifact_preview_serves_inline_content(staff_client, processing, article): + artifact = save_artifact( + processing, + ArtifactType.HTML, + "preview.html", + b"Preview", + article=article, + ) + + response = staff_client.get( + reverse("manuscripts:artifact_preview", kwargs={"pk": artifact.pk}), + ) + + assert response.status_code == 200 + assert response["Content-Type"].startswith("text/html") + assert b"Preview" in b"".join(response.streaming_content) + + +@pytest.mark.django_db +def test_article_validation_view_json_array_report(request_factory, staff_user, article, processing, mock_render): + save_artifact( + processing, + ArtifactType.VALIDATION_REPORT, + "report.json", + json.dumps([{"group": "meta", "response": "ok"}]).encode(), + article=article, + ) + request = _staff_request( + request_factory, + staff_user, + "get", + reverse("manuscripts:article_validation", kwargs={"pk": article.pk}), + ) + + response = editorial_views.article_validation_view(request, article.pk) + + assert response.status_code == 200 + assert mock_render["context"]["rows"] == [{"group": "meta", "response": "ok"}] + + +@pytest.mark.django_db +def test_article_validation_view_json_object_report(request_factory, staff_user, article, processing, mock_render): + save_artifact( + processing, + ArtifactType.VALIDATION_REPORT, + "report.json", + json.dumps({"group": "single"}).encode(), + article=article, + ) + request = _staff_request( + request_factory, + staff_user, + "get", + reverse("manuscripts:article_validation", kwargs={"pk": article.pk}), + ) + + editorial_views.article_validation_view(request, article.pk) + + assert mock_render["context"]["rows"] == [{"group": "single"}] + + +@pytest.mark.django_db +def test_article_validation_view_json_lines_report(request_factory, staff_user, article, processing, mock_render): + save_artifact( + processing, + ArtifactType.VALIDATION_REPORT, + "report.jsonl", + b'{"group": "line-1"}\n{"group": "line-2"}', + article=article, + ) + request = _staff_request( + request_factory, + staff_user, + "get", + reverse("manuscripts:article_validation", kwargs={"pk": article.pk}), + ) + + editorial_views.article_validation_view(request, article.pk) + + assert len(mock_render["context"]["rows"]) == 2 + + +@pytest.mark.django_db +def test_article_validation_view_csv_report(request_factory, staff_user, article, processing, mock_render): + csv_content = "context,response,detail,advice\nmeta,error,wrong value,fix it\n" + save_artifact( + processing, + ArtifactType.VALIDATION_REPORT, + "report.csv", + csv_content.encode(), + article=article, + ) + request = _staff_request( + request_factory, + staff_user, + "get", + reverse("manuscripts:article_validation", kwargs={"pk": article.pk}), + ) + + editorial_views.article_validation_view(request, article.pk) + + rows = mock_render["context"]["rows"] + assert rows[0]["group"] == "meta" + assert rows[0]["response"] == "error" + assert rows[0]["got_value"] == "wrong value" + assert rows[0]["advice"] == "fix it" + + +@pytest.mark.django_db +def test_article_validation_view_exceptions_and_packtools( + request_factory, staff_user, article, processing, mock_render, monkeypatch +): + save_artifact( + processing, + ArtifactType.VALIDATION_EXCEPTIONS, + "exceptions.json", + json.dumps([{"rule": "x"}]).encode(), + article=article, + ) + xml_artifact = save_artifact( + processing, + ArtifactType.XML, + "article.xml", + b"
    ", + article=article, + ) + + validator = MagicMock() + validator.validate_all.return_value = (False, [MagicMock(message="schema error", line=3)]) + validator.annotate_errors.return_value = etree.Element("article") + monkeypatch.setattr("manuscripts.views.editorial.packtools.XMLValidator.parse", lambda _path: validator) + + request = _staff_request( + request_factory, + staff_user, + "get", + reverse("manuscripts:article_validation", kwargs={"pk": article.pk}), + ) + + editorial_views.article_validation_view(request, article.pk) + + assert mock_render["context"]["exceptions"] == [{"rule": "x"}] + assert mock_render["context"]["schema_valid"] is False + assert mock_render["context"]["schema_errors"] == [{"message": "schema error", "line": 3}] + assert mock_render["context"]["annotated_xml"] is not None + + +@pytest.mark.django_db +def test_article_validation_view_handles_packtools_errors( + request_factory, staff_user, article, processing, mock_render, monkeypatch +): + xml_artifact = save_artifact( + processing, + ArtifactType.XML, + "article.xml", + b"
    ", + article=article, + ) + monkeypatch.setattr( + "manuscripts.views.editorial.packtools.XMLValidator.parse", + MagicMock(side_effect=RuntimeError("packtools failed")), + ) + request = _staff_request( + request_factory, + staff_user, + "get", + reverse("manuscripts:article_validation", kwargs={"pk": article.pk}), + ) + + editorial_views.article_validation_view(request, article.pk) + + assert mock_render["context"]["schema_errors"] == [{"message": "packtools failed", "line": None}] + + +@pytest.mark.django_db +def test_article_structure_edit_redirects_without_structure(request_factory, staff_user, article): + request = _staff_request( + request_factory, + staff_user, + "get", + reverse("manuscripts:article_structure_edit", kwargs={"pk": article.pk}), + ) + + response = editorial_views.article_structure_edit(request, article.pk) + + assert response.status_code == 302 + + +@pytest.mark.django_db +def test_article_structure_edit_get_renders_version( + request_factory, staff_user, article, processing, mock_render +): + structure = create_structure_version(article, processing, InputType.XML, [], [], []) + request = request_factory.get( + reverse("manuscripts:article_structure_edit", kwargs={"pk": article.pk}), + {"version": str(structure.version)}, + ) + request.user = staff_user + request.session = {} + + response = editorial_views.article_structure_edit(request, article.pk) + + assert response.status_code == 200 + assert mock_render["context"]["structure"] == structure + assert mock_render["context"]["is_historical_version"] is False + + +@pytest.mark.django_db +def test_article_structure_edit_post_valid_saves_structure( + request_factory, staff_user, article, processing, monkeypatch +): + structure = create_structure_version(article, processing, InputType.XML, [], [], []) + saved = MagicMock(version=2) + monkeypatch.setattr("manuscripts.views.editorial._save_corrected_structure", lambda *args, **kwargs: saved) + + form = MagicMock() + form.is_valid.return_value = True + form.cleaned_data = {"front": [], "body": [], "back": []} + handler = MagicMock() + handler.get_form_class.return_value = MagicMock(return_value=form) + handler.get_bound_panel.return_value = "bound-panel" + monkeypatch.setattr( + "manuscripts.views.editorial.ObjectList", + MagicMock(return_value=MagicMock(bind_to_model=MagicMock(return_value=handler))), + ) + + request = _staff_request( + request_factory, + staff_user, + "post", + reverse("manuscripts:article_structure_edit", kwargs={"pk": article.pk}), + {}, + ) + + response = editorial_views.article_structure_edit(request, article.pk) + + assert response.status_code == 302 + + +@pytest.mark.django_db +def test_article_structure_edit_post_invalid_renders( + request_factory, staff_user, article, processing, mock_render, monkeypatch +): + create_structure_version(article, processing, InputType.XML, [], [], []) + form = MagicMock() + form.is_valid.return_value = False + handler = MagicMock() + handler.get_form_class.return_value = MagicMock(return_value=form) + handler.get_bound_panel.return_value = "bound-panel" + monkeypatch.setattr( + "manuscripts.views.editorial.ObjectList", + MagicMock(return_value=MagicMock(bind_to_model=MagicMock(return_value=handler))), + ) + request = _staff_request( + request_factory, + staff_user, + "post", + reverse("manuscripts:article_structure_edit", kwargs={"pk": article.pk}), + {}, + ) + + response = editorial_views.article_structure_edit(request, article.pk) + + assert response.status_code == 200 + assert mock_render["template"] == "manuscripts/article_structure_edit.html" + + +@pytest.mark.django_db +def test_article_structure_revert_creates_new_version(request_factory, staff_user, article, processing, monkeypatch): + structure = create_structure_version( + article, + processing, + InputType.XML, + [], + [{"type": "paragraph", "value": {"label": "

    ", "paragraph": "Body"}}], + [], + ) + new_structure = MagicMock(version=2) + monkeypatch.setattr("manuscripts.views.editorial._save_corrected_structure", lambda *args, **kwargs: new_structure) + request = _staff_request( + request_factory, + staff_user, + "post", + reverse( + "manuscripts:article_structure_revert", + kwargs={"pk": article.pk, "version": structure.version}, + ), + {}, + ) + + response = editorial_views.article_structure_revert(request, article.pk, structure.version) + + assert response.status_code == 302 + + +@pytest.mark.django_db +def test_article_references_edit_redirects_without_structure(request_factory, staff_user, article): + request = _staff_request( + request_factory, + staff_user, + "get", + reverse("manuscripts:article_references_edit", kwargs={"pk": article.pk}), + ) + + response = editorial_views.article_references_edit(request, article.pk) + + assert response.status_code == 302 + + +@pytest.mark.django_db +def test_article_references_edit_renders_references( + request_factory, staff_user, article, processing, mock_render +): + structure = create_structure_version( + article, + processing, + InputType.XML, + [], + [], + [{"type": "ref_paragraph", "value": {"label": "

    ", "paragraph": "Ref", "refid": "B1"}}], + ) + sync_references(structure) + request = _staff_request( + request_factory, + staff_user, + "get", + reverse("manuscripts:article_references_edit", kwargs={"pk": article.pk}), + ) + + response = editorial_views.article_references_edit(request, article.pk) + + assert response.status_code == 200 + assert mock_render["context"]["structure"] == structure + assert mock_render["context"]["references"].count() == 1 + + +@pytest.mark.django_db +def test_citation_update_rewrites_xref_and_saves_structure(request_factory, staff_user, article, processing): + structure = create_structure_version( + article, + processing, + InputType.XML, + [], + [{"type": "paragraph", "value": {"label": "

    ", "paragraph": 'See Smith, 2020.'}}], + [{"type": "ref_paragraph", "value": {"label": "

    ", "paragraph": "Smith J. 2020.", "refid": "B1"}}], + ) + sync_references(structure) + citation = structure.citations.get() + reference = structure.references.get() + request = _staff_request( + request_factory, + staff_user, + "post", + reverse("manuscripts:citation_update", kwargs={"pk": citation.pk}), + {"references": [str(reference.pk)]}, + ) + + response = editorial_views.citation_update(request, citation.pk) + + assert response.status_code == 302 + new_structure = article.structure_versions.order_by("-version").first() + assert new_structure.version == 2 + paragraph = new_structure.body[0].value["paragraph"] + assert 'rid="B1"' in paragraph + + +@pytest.mark.django_db +def test_citation_update_without_matching_block_still_saves(request_factory, staff_user, article, processing, monkeypatch): + structure = create_structure_version(article, processing, InputType.XML, [], [], []) + citation = CitationOccurrence.objects.create( + structure=structure, + text="Missing", + location={"block": 99}, + status=CitationOccurrence.Status.ORPHAN, + ) + monkeypatch.setattr("manuscripts.views.editorial._save_corrected_structure", MagicMock()) + request = _staff_request( + request_factory, + staff_user, + "post", + reverse("manuscripts:citation_update", kwargs={"pk": citation.pk}), + {}, + ) + + response = editorial_views.citation_update(request, citation.pk) + + assert response.status_code == 302 + + +@pytest.mark.django_db +def test_article_reference_select_updates_selected_element(request_factory, staff_user, article, processing): + structure = create_structure_version( + article, + processing, + InputType.XML, + [], + [], + [{"type": "ref_paragraph", "value": {"label": "

    ", "paragraph": "Smith J. 2020.", "refid": "B1"}}], + ) + sync_references(structure) + article_reference = structure.references.get() + reference = article_reference.reference + element = ElementCitation.objects.create( + reference=reference, + marked={"authors": ["Smith J"]}, + sort_order=0, + ) + request = _staff_request( + request_factory, + staff_user, + "post", + reverse("manuscripts:article_reference_select", kwargs={"pk": article_reference.pk}), + {"selected_element": str(element.pk)}, + ) + + response = editorial_views.article_reference_select(request, article_reference.pk) + + assert response.status_code == 302 + new_reference = article.structure_versions.order_by("-version").first().references.get(ref_id="B1") + assert new_reference.selected_element_id == element.pk + + +@pytest.mark.django_db +def test_article_reference_select_without_candidate(request_factory, staff_user, article, processing): + structure = create_structure_version( + article, + processing, + InputType.XML, + [], + [], + [{"type": "ref_paragraph", "value": {"label": "

    ", "paragraph": "Smith J. 2020.", "refid": "B1"}}], + ) + sync_references(structure) + article_reference = structure.references.get() + request = _staff_request( + request_factory, + staff_user, + "post", + reverse("manuscripts:article_reference_select", kwargs={"pk": article_reference.pk}), + {}, + ) + + response = editorial_views.article_reference_select(request, article_reference.pk) + + assert response.status_code == 302 + + +@pytest.mark.django_db +def test_article_reprocess_without_processing_redirects(request_factory, staff_user, article): + request = _staff_request( + request_factory, + staff_user, + "post", + reverse("manuscripts:article_reprocess", kwargs={"pk": article.pk}), + {}, + ) + + response = editorial_views.article_reprocess(request, article.pk) + + assert response.status_code == 302 + messages = [message.message for message in get_messages(request)] + assert any("no associated processings" in message.lower() for message in messages) + + +@pytest.mark.django_db +def test_article_validation_view_handles_report_read_errors( + request_factory, staff_user, article, processing, mock_render +): + artifact = save_artifact( + processing, + ArtifactType.VALIDATION_REPORT, + "broken.json", + b"[", + article=article, + ) + artifact.file.storage.delete(artifact.file.name) + request = _staff_request( + request_factory, + staff_user, + "get", + reverse("manuscripts:article_validation", kwargs={"pk": article.pk}), + ) + + editorial_views.article_validation_view(request, article.pk) + + assert mock_render["context"]["rows"] == [] + + +@pytest.mark.django_db +def test_article_validation_view_parses_exception_json_lines( + request_factory, staff_user, article, processing, mock_render +): + save_artifact( + processing, + ArtifactType.VALIDATION_EXCEPTIONS, + "exceptions.jsonl", + b'{"rule": "line-1"}\n{"rule": "line-2"}', + article=article, + ) + request = _staff_request( + request_factory, + staff_user, + "get", + reverse("manuscripts:article_validation", kwargs={"pk": article.pk}), + ) + + editorial_views.article_validation_view(request, article.pk) + + assert mock_render["context"]["exceptions"] == [{"rule": "line-1"}, {"rule": "line-2"}] + + +@pytest.mark.django_db +def test_article_validation_view_wraps_single_exception_object( + request_factory, staff_user, article, processing, mock_render +): + save_artifact( + processing, + ArtifactType.VALIDATION_EXCEPTIONS, + "exceptions.json", + b'{"rule": "single"}', + article=article, + ) + request = _staff_request( + request_factory, + staff_user, + "get", + reverse("manuscripts:article_validation", kwargs={"pk": article.pk}), + ) + + editorial_views.article_validation_view(request, article.pk) + + assert mock_render["context"]["exceptions"] == [{"rule": "single"}] + + +@pytest.mark.django_db +def test_article_validation_view_handles_exception_read_errors( + request_factory, staff_user, article, processing, mock_render +): + artifact = save_artifact( + processing, + ArtifactType.VALIDATION_EXCEPTIONS, + "broken.json", + b"[]", + article=article, + ) + artifact.file.storage.delete(artifact.file.name) + request = _staff_request( + request_factory, + staff_user, + "get", + reverse("manuscripts:article_validation", kwargs={"pk": article.pk}), + ) + + editorial_views.article_validation_view(request, article.pk) + + assert mock_render["context"]["exceptions"] == [] + + +@pytest.mark.django_db +def test_article_reprocess_ignores_invalid_action(request_factory, staff_user, article, processing, monkeypatch): + processing.articles.add(article) + delay = MagicMock() + monkeypatch.setattr("manuscripts.views.editorial.process_input.delay", delay) + request = _staff_request( + request_factory, + staff_user, + "post", + reverse("manuscripts:article_reprocess", kwargs={"pk": article.pk}), + {"action": "invalid-action"}, + ) + + editorial_views.article_reprocess(request, article.pk) + + delay.assert_called_once_with(processing.pk, start_action=None) + + +@pytest.mark.django_db +def test_article_reprocess_dispatches_task(request_factory, staff_user, article, processing, monkeypatch): + processing.articles.add(article) + delay = MagicMock() + monkeypatch.setattr("manuscripts.views.editorial.process_input.delay", delay) + request = _staff_request( + request_factory, + staff_user, + "post", + reverse("manuscripts:article_reprocess", kwargs={"pk": article.pk}), + {"action": ProcessingAction.XML_VALIDATION}, + ) + + response = editorial_views.article_reprocess(request, article.pk) + + processing.refresh_from_db() + assert response.status_code == 302 + assert processing.retry_count == 1 + delay.assert_called_once_with(processing.pk, start_action=ProcessingAction.XML_VALIDATION) + + +@pytest.mark.django_db +def test_delete_artifacts_removes_files(processing, article): + artifacts = [ + save_artifact(processing, ArtifactType.XML, "one.xml", b"", article=article), + save_artifact(processing, ArtifactType.HTML, "two.html", b"", article=article), + ] + + deleted = editorial_views._delete_artifacts(artifacts) + + assert deleted == 2 + assert ArticleArtifact.objects.count() == 0 + + +@pytest.mark.django_db +def test_confirmed_cleanup_helper(request_factory, staff_user): + request = _staff_request(request_factory, staff_user, "post", "/", {"confirmation": "limpar"}) + assert editorial_views._confirmed_cleanup(request) is True + request = _staff_request(request_factory, staff_user, "post", "/", {"confirmation": "nope"}) + assert editorial_views._confirmed_cleanup(request) is False + + +@pytest.mark.django_db +def test_ensure_current_structure_helper(processing, article): + latest = ArticleStructureVersion.objects.create( + article=article, + processing=processing, + version=1, + is_current=False, + source_kind=InputType.XML, + front=[], + body=[], + back=[], + ) + + assert editorial_views._ensure_current_structure(article) is True + latest.refresh_from_db() + assert latest.is_current is True + assert editorial_views._ensure_current_structure(article) is True + + +@pytest.mark.django_db +def test_save_corrected_structure_helper(processing, article, monkeypatch): + structure = create_structure_version(article, processing, InputType.XML, [], [], []) + monkeypatch.setattr( + "manuscripts.views.editorial.generate_structure_xml", + lambda _structure: b"

    ", + ) + + new_structure = editorial_views._save_corrected_structure( + article, + structure, + [], + [], + [], + ) + + assert new_structure.version == 2 + assert article.current_artifact(ArtifactType.XML) is not None diff --git a/manuscripts/tests/test_forms.py b/manuscripts/tests/test_forms.py new file mode 100644 index 0000000..165709f --- /dev/null +++ b/manuscripts/tests/test_forms.py @@ -0,0 +1,216 @@ +import io +import zipfile + +import pytest +from django.core.files.uploadedfile import SimpleUploadedFile + +from manuscripts.choices import InputType, ProcessingAction +from manuscripts.forms import ( + DOCXUploadForm, + ProcessingReviewForm, + ProcessingUploadForm, + SPSPackageUploadForm, + XMLUploadForm, +) +from manuscripts.models.processing import Processing + + +@pytest.mark.django_db +def test_processing_upload_form_accepts_xml_file(): + form = ProcessingUploadForm( + data={"title": "XML upload"}, + files={"input_file": SimpleUploadedFile("article.xml", b"
    ", content_type="application/xml")}, + ) + + assert form.is_valid(), form.errors + + +@pytest.mark.django_db +def test_processing_upload_form_rejects_unsupported_extension(): + form = ProcessingUploadForm( + data={"title": "Bad upload"}, + files={"input_file": SimpleUploadedFile("article.txt", b"plain text", content_type="text/plain")}, + ) + + assert not form.is_valid() + assert "input_file" in form.errors + + +@pytest.mark.django_db +def test_processing_upload_form_rejects_empty_file(): + form = ProcessingUploadForm( + data={"title": "Empty upload"}, + files={"input_file": SimpleUploadedFile("article.xml", b"", content_type="application/xml")}, + ) + + assert not form.is_valid() + assert "input_file" in form.errors + + +@pytest.mark.django_db +def test_processing_upload_form_clean_input_file_rejects_empty_upload(): + form = ProcessingUploadForm() + uploaded = SimpleUploadedFile("article.xml", b"", content_type="application/xml") + form.cleaned_data = {"input_file": uploaded} + + from django.core.exceptions import ValidationError + + with pytest.raises(ValidationError, match="empty"): + form.clean_input_file() + + +@pytest.mark.django_db +def test_processing_review_form_accepts_applicable_actions(xml_processing): + form = ProcessingReviewForm( + data={"requested_actions": [ProcessingAction.XML_VALIDATION]}, + instance=xml_processing, + ) + + assert form.is_valid(), form.errors + + +@pytest.mark.django_db +def test_processing_review_form_rejects_incompatible_actions(user): + processing = Processing.objects.create( + title="doc", + creator=user, + detected_type=InputType.DOCUMENT, + input_file="processings/input/doc.docx", + ) + form = ProcessingReviewForm( + data={"requested_actions": [ProcessingAction.SPS_PACKAGE_VALIDATION]}, + instance=processing, + ) + + assert not form.is_valid() + assert "requested_actions" in form.errors + + +@pytest.mark.django_db +def test_processing_upload_form_rejects_oversized_file(): + form = ProcessingUploadForm( + data={"title": "Huge upload"}, + files={ + "input_file": SimpleUploadedFile( + "article.xml", + b"x" * (250 * 1024 * 1024 + 1), + content_type="application/xml", + ) + }, + ) + + assert not form.is_valid() + assert "input_file" in form.errors + + +@pytest.mark.django_db +def test_processing_upload_form_rejects_invalid_zip(): + form = ProcessingUploadForm( + data={"title": "Bad zip"}, + files={"input_file": SimpleUploadedFile("package.zip", b"not-a-zip", content_type="application/zip")}, + ) + + assert not form.is_valid() + assert "input_file" in form.errors + + +@pytest.mark.django_db +def test_xml_upload_form_rejects_non_xml_extension(): + form = XMLUploadForm( + data={"title": "Wrong ext"}, + files={"input_file": SimpleUploadedFile("article.docx", b"docx", content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document")}, + ) + + assert not form.is_valid() + assert "input_file" in form.errors + + +@pytest.mark.django_db +def test_docx_upload_form_rejects_non_docx_extension(): + form = DOCXUploadForm( + data={"title": "Wrong ext"}, + files={"input_file": SimpleUploadedFile("article.xml", b"
    ", content_type="application/xml")}, + ) + + assert not form.is_valid() + assert "input_file" in form.errors + + +@pytest.mark.django_db +def test_sps_package_upload_form_rejects_non_zip_extension(): + form = SPSPackageUploadForm( + data={"title": "Wrong ext"}, + files={"input_file": SimpleUploadedFile("article.xml", b"
    ", content_type="application/xml")}, + ) + + assert not form.is_valid() + assert "input_file" in form.errors + + +@pytest.mark.django_db +def test_processing_review_form_sps_package_includes_html_pdf_actions(user): + processing = Processing.objects.create( + title="sps", + creator=user, + detected_type=InputType.SPS_PACKAGE, + input_file="processings/input/package.zip", + ) + form = ProcessingReviewForm(instance=processing) + + action_values = {value for value, _label in form.fields["requested_actions"].choices} + assert ProcessingAction.HTML_GENERATION in action_values + assert ProcessingAction.PDF_GENERATION in action_values + + +@pytest.mark.django_db +def test_processing_review_form_clean_rejects_invalid_sps_actions(user, monkeypatch): + processing = Processing.objects.create( + title="sps", + creator=user, + detected_type=InputType.SPS_PACKAGE, + input_file="processings/input/package.zip", + ) + form = ProcessingReviewForm(instance=processing) + form.cleaned_data = {"requested_actions": [ProcessingAction.CITATION_MARKUP]} + monkeypatch.setattr("manuscripts.forms.suggested_actions", lambda _input_type: []) + + from django.core.exceptions import ValidationError + + with pytest.raises(ValidationError, match="incompatible"): + form.clean() + + +@pytest.mark.django_db +def test_xml_upload_form_accepts_xml_file(): + form = XMLUploadForm( + data={"title": "XML upload"}, + files={"input_file": SimpleUploadedFile("article.xml", b"
    ", content_type="application/xml")}, + ) + + assert form.is_valid(), form.errors + + +@pytest.mark.django_db +def test_docx_upload_form_accepts_docx_file(): + buffer = io.BytesIO() + with zipfile.ZipFile(buffer, "w") as archive: + archive.writestr("word/document.xml", "") + form = DOCXUploadForm( + data={"title": "DOCX upload"}, + files={"input_file": SimpleUploadedFile("article.docx", buffer.getvalue(), content_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document")}, + ) + + assert form.is_valid(), form.errors + + +@pytest.mark.django_db +def test_sps_package_upload_form_accepts_zip_file(): + buffer = io.BytesIO() + with zipfile.ZipFile(buffer, "w") as archive: + archive.writestr("article.xml", "
    ") + form = SPSPackageUploadForm( + data={"title": "ZIP upload"}, + files={"input_file": SimpleUploadedFile("package.zip", buffer.getvalue(), content_type="application/zip")}, + ) + + assert form.is_valid(), form.errors diff --git a/manuscripts/tests/test_frontmatter.py b/manuscripts/tests/test_frontmatter.py new file mode 100644 index 0000000..8937680 --- /dev/null +++ b/manuscripts/tests/test_frontmatter.py @@ -0,0 +1,285 @@ +import pytest + +from ai.utils.blocks import make_block, make_lang_block +from manuscripts.utils.frontmatter import ( + _collect_payload_texts, + _collect_remaining_blocks, + _is_label_replaced, + _is_text_covered, + enrich_frontmatter, +) + + +def _paragraph(label, text, language=None): + block_type = "paragraph_with_language" if language else "paragraph" + value = {"label": label, "paragraph": text} + if language: + value["language"] = language + return {"type": block_type, "value": value} + + +def _long_text(prefix="x", length=90): + return prefix + ("a" * (length - len(prefix))) + + +@pytest.mark.django_db +def test_enrich_frontmatter_builds_all_block_types(article): + original_front = [ + _paragraph("", "Resumen", "es"), + _paragraph("", "Original Spanish abstract", "es"), + _paragraph("

    ", "Keep this orphan paragraph"), + ] + payload = { + "doi": "10.1234/example", + "titles": [ + {"text": "Main Title", "language": "en"}, + {"text": "Título", "language": "es"}, + ], + "authors": [ + { + "display": "Smith, John", + "surname": "Smith", + "given_names": "John", + "orcid": "0000-0001-2345-6789", + "affiliations": ["aff1"], + "symbol": "*", + } + ], + "affiliations": [ + { + "id": "aff1", + "text": "University of Example", + "symbol": "†", + "orgname": "Example U", + "orgdiv2": "Dept", + "orgdiv1": "Faculty", + "city": "City", + "state": "ST", + "country": "Country", + "country_code": "XX", + } + ], + "dates": [ + {"type": "received", "date": "2024-01-01"}, + {"type": "accepted", "raw": "2024-06-01"}, + ], + "abstracts": [ + {"text": "New English abstract", "language": "en"}, + {"text": "Novo resumo", "language": "pt"}, + ], + "keywords": [ + {"title": "Keywords", "terms": ["alpha", "beta"], "language": "en"}, + {"terms": ["gamma"], "language": "pt"}, + ], + } + + blocks = enrich_frontmatter(original_front, payload, article) + + assert blocks[0] == make_block("", "10.1234/example") + assert blocks[1] == make_lang_block("", "Main Title", "en") + assert blocks[2] == make_lang_block("", "Título", "es") + assert blocks[3]["type"] == "author_paragraph" + assert blocks[3]["value"]["affid"] == "aff1" + assert blocks[4]["type"] == "aff_paragraph" + assert blocks[4]["value"]["code_country"] == "XX" + assert make_block("", "2024-01-01") in blocks + assert make_block("", "2024-06-01") in blocks + assert make_lang_block("", "New English abstract", "en") in blocks + assert make_lang_block("", "Resumo", "pt") in blocks + assert make_lang_block("", "Keywords", "en") in blocks + assert make_lang_block("", "alpha; beta", "en") in blocks + assert make_lang_block("", "gamma", "pt") in blocks + assert _paragraph("

    ", "Keep this orphan paragraph") in blocks + + +@pytest.mark.django_db +def test_enrich_frontmatter_detects_abstract_language_from_title(article): + for title_text, lang in [("Resumen", "es"), ("Resumo", "pt"), ("Abstract", "en")]: + original_front = [ + _paragraph("", title_text), + _paragraph("", f"Existing {lang} abstract"), + ] + payload = {"abstracts": [{"text": "Skipped duplicate", "language": lang}]} + blocks = enrich_frontmatter(original_front, payload, article) + assert not any( + b.get("value", {}).get("label") == "" + and b["value"].get("paragraph") == "Skipped duplicate" + for b in blocks + ) + + +@pytest.mark.django_db +def test_enrich_frontmatter_abstract_title_without_language_uses_detected_lang(article): + original_front = [ + _paragraph("", "Abstract"), + _paragraph("", "Existing abstract body"), + ] + payload = {"abstracts": [{"text": "Should be skipped", "language": "en"}]} + blocks = enrich_frontmatter(original_front, payload, article) + assert not any( + b.get("value", {}).get("label") == "" and b["value"].get("paragraph") == "Should be skipped" + for b in blocks + ) + + +@pytest.mark.django_db +def test_enrich_frontmatter_uses_article_language_fallback(article): + article.language = "" + article.save(update_fields=["language"]) + payload = { + "titles": [{"text": "Untitled language"}], + "keywords": [{"terms": ["one"]}], + } + blocks = enrich_frontmatter([], payload, article) + assert blocks[0] == make_lang_block("", "Untitled language", "en") + assert make_lang_block("", "one", "en") in blocks + + +@pytest.mark.parametrize( + ("label", "payload", "expected"), + [ + ("", {"doi": "10.1/x"}, True), + ("", {}, False), + ("", {"titles": [{"text": "T"}]}, True), + ("", {"titles": [{"text": "T"}]}, True), + ("", {}, False), + ("", {"authors": [{"display": "A"}]}, True), + ("", {}, False), + ("", {"affiliations": [{"id": "1", "text": "X"}]}, True), + ("", {}, False), + ("", {"dates": [{"type": "received"}]}, True), + ("", {"dates": [{"type": "accepted"}]}, False), + ("", {"dates": [{"type": "accepted"}]}, True), + ("", {}, False), + ("", {"dates": [{"type": "received"}]}, True), + ("", {}, False), + ("", {"abstracts": [{"text": "a"}]}, False), + ("", {}, False), + ("", {}, False), + ("", {"keywords": [{"terms": ["k"]}]}, True), + ("", {}, False), + ("", {"doi": "10.1/x"}, False), + ], +) +def test_is_label_replaced(label, payload, expected): + assert _is_label_replaced(label, payload) is expected + + +def test_collect_payload_texts_gathers_normalized_unique_values(): + payload = { + "doi": "10.1234/DOI", + "titles": [{"text": " Title "}], + "abstracts": [{"title": "Abs", "text": "Body"}], + "keywords": [{"title": "Keys", "terms": ["a", "b"]}], + "dates": [{"date": "2024", "raw": "raw-date"}], + } + texts = _collect_payload_texts(payload) + assert "10.1234/doi" in texts + assert "title" in texts + assert "abs" in texts + assert "body" in texts + assert "keys" in texts + assert "a; b" in texts + assert "a, b" in texts + assert "2024" in texts + assert "raw-date" in texts + + +def test_collect_remaining_blocks_skips_replaced_and_covered_labels(): + payload = { + "doi": "10.1/x", + "titles": [{"text": "New title"}], + "authors": [{"display": "Author"}], + "affiliations": [{"id": "1", "text": "Aff"}], + "keywords": [{"terms": ["kw"]}], + } + front = [ + _paragraph("", "old doi"), + _paragraph("", "old title"), + _paragraph("<author-notes>", "notes"), + _paragraph("<p>", "new title"), + _paragraph("<p>", "unique paragraph"), + ] + kept = _collect_remaining_blocks(front, payload) + labels = [item["value"]["label"] for item in kept] + assert "<article-id>" not in labels + assert "<title>" not in labels + assert "<author-notes>" not in labels + assert any(item["value"]["paragraph"] == "unique paragraph" for item in kept) + + +def test_collect_remaining_blocks_keeps_author_notes_without_payload_authors(): + front = [_paragraph("<author-notes>", "editor note")] + kept = _collect_remaining_blocks(front, {}) + assert kept == [{"type": "paragraph", "value": {"label": "<author-notes>", "paragraph": "editor note"}}] + + +def test_is_text_covered_empty_and_exact_match(): + seen = {"hello world"} + assert _is_text_covered("", seen) is True + assert _is_text_covered(" Hello World ", seen) is True + + +def test_is_text_covered_long_overlap_branches(): + long_a = _long_text("a", 90) + long_b = _long_text("b", 90) + assert _is_text_covered(long_a, {long_a[:85]}) is True + assert _is_text_covered(long_a + " tail", {long_a}) is True + assert _is_text_covered("prefix " + long_b, {long_b}) is True + + +def test_is_text_covered_strips_tags_and_matches_seen(): + tagged = "<b>Keyword</b> section" + seen = {"keyword section"} + assert _is_text_covered(tagged, seen) is True + long_inner = _long_text("inner", 70) + tagged_long = f"<i>{long_inner}</i>" + assert _is_text_covered(tagged_long + " suffix", {long_inner}) is True + + +@pytest.mark.parametrize( + "text", + [ + "This is the resumen of the paper", + "Este es el resumo final", + "Paper abstract goes here", + "Palabras clave: science", + "Palavras-chave: ciência", + "Keywords: biology", + "Received: 2020", + "Recibido enero 2020", + "Recebido em 2020", + "Accepted March 2021", + "Aceptado marzo 2021", + "Aceito em 2021", + "Published online 2022", + "Publicado em 2022", + "doi: 10.1234/5678/sample", + "Contact orcid.org/0000-0001-2345-6789", + "Author 0000-0001-2345-6789", + "Reach us at author@example.org", + ], +) +def test_is_text_covered_heuristic_patterns(text): + assert _is_text_covered(text, set()) is True + + +def test_is_text_covered_matches_seen_item_after_tag_removal(monkeypatch): + import re + + import manuscripts.utils.frontmatter as frontmatter_module + + item = "a" * 65 + text = "prefix " + item[:30] + "<b>" + item[30:60] + "</b>" + item[60:] + + class _TagRe: + @staticmethod + def sub(repl, value, count=0): + return re.sub(r"<[^>]+>", "", value) + + monkeypatch.setattr(frontmatter_module, "_TAG_RE", _TagRe()) + assert frontmatter_module._is_text_covered(text, {item}) is True + + +def test_is_text_covered_returns_false_for_unmatched_text(): + assert _is_text_covered("Short unique body paragraph.", {"other"}) is False diff --git a/manuscripts/tests/test_helpers.py b/manuscripts/tests/test_helpers.py new file mode 100644 index 0000000..42f6a48 --- /dev/null +++ b/manuscripts/tests/test_helpers.py @@ -0,0 +1,48 @@ +from types import SimpleNamespace + +from manuscripts.utils.helpers import _block, _raw_reference, to_dict_list + + +def test_block_returns_paragraph_structure(): + assert _block("<p>", "Hello") == { + "type": "paragraph", + "value": {"label": "<p>", "paragraph": "Hello"}, + } + + +def test_block_accepts_custom_block_type(): + assert _block("<sec>", "Section", block_type="paragraph_with_language") == { + "type": "paragraph_with_language", + "value": {"label": "<sec>", "paragraph": "Section"}, + } + + +def test_raw_reference_builds_ref_paragraph(): + assert _raw_reference(3, "Citation text.") == { + "type": "ref_paragraph", + "value": { + "label": "<p>", + "refid": "B3", + "paragraph": "Citation text.", + "authors": [], + }, + } + + +def test_to_dict_list_returns_empty_for_falsy_values(): + assert to_dict_list(None) == [] + assert to_dict_list([]) == [] + + +def test_to_dict_list_returns_plain_list_unchanged(): + data = [{"type": "paragraph", "value": {"label": "<p>", "paragraph": "x"}}] + assert to_dict_list(data) is data + + +def test_to_dict_list_uses_stream_block_get_prep_value(): + class FakeStreamValue: + stream_block = SimpleNamespace( + get_prep_value=lambda stream: [{"type": "paragraph", "value": {"prepared": True}}] + ) + + assert to_dict_list(FakeStreamValue()) == [{"type": "paragraph", "value": {"prepared": True}}] diff --git a/manuscripts/tests/test_ingestion.py b/manuscripts/tests/test_ingestion.py new file mode 100644 index 0000000..5e18a68 --- /dev/null +++ b/manuscripts/tests/test_ingestion.py @@ -0,0 +1,178 @@ +import zipfile + +import pytest +from django.core.files.uploadedfile import SimpleUploadedFile + +from manuscripts.choices import ArtifactType, InputType +from manuscripts.models.article import ArticleArtifact +from manuscripts.models.processing import Processing +from manuscripts.utils.ingestion import ingest_document, ingest_xml, ingest_zip + +MINIMAL_XML = b"<article></article>" + + +def _processing_with_file(user, filename, content, confirmed_type=InputType.XML): + uploaded = SimpleUploadedFile(filename, content, content_type="application/octet-stream") + processing = Processing.objects.create( + title="Ingestion test", + creator=user, + input_file=uploaded, + confirmed_type=confirmed_type, + ) + return processing + + +def _write_zip(path, members): + with zipfile.ZipFile(path, "w") as archive: + for name, data in members: + archive.writestr(name, data) + + +@pytest.mark.django_db +def test_ingest_xml_creates_article_artifact_and_structure(processing, monkeypatch): + processing.confirmed_type = InputType.XML + processing.save(update_fields=["confirmed_type"]) + + monkeypatch.setattr( + "manuscripts.utils.ingestion.parse_xml_structure", + lambda content: ([], [], [], []), + ) + + article, artifact = ingest_xml(processing, "article.xml", MINIMAL_XML, "pkg/article.xml") + + assert article.pk is not None + assert processing.articles.filter(pk=article.pk).exists() + assert artifact.artifact_type == ArtifactType.XML + assert artifact.original_path == "pkg/article.xml" + assert artifact.metadata["referenced_assets"] == [] + artifact.refresh_from_db() + assert artifact.structure is not None + assert artifact.structure.article_id == article.pk + + +@pytest.mark.django_db +def test_ingest_document_saves_source_document(user): + processing = _processing_with_file(user, "manuscript.docx", b"docx-bytes", InputType.DOCUMENT) + + artifact = ingest_document(processing) + + assert artifact.artifact_type == ArtifactType.SOURCE_DOCUMENT + assert artifact.file.read() == b"docx-bytes" + + +@pytest.mark.django_db +def test_ingest_document_source_package_returns_docx_from_zip(user, tmp_path): + zip_path = tmp_path / "source.zip" + _write_zip(zip_path, [("article.docx", b"docx-content"), ("notes.txt", b"ignore")]) + processing = _processing_with_file( + user, + "source.zip", + zip_path.read_bytes(), + InputType.SOURCE_PACKAGE, + ) + + artifact = ingest_document(processing) + + assert artifact.artifact_type == ArtifactType.SOURCE_DOCUMENT + assert artifact.file.read() == b"docx-content" + + +@pytest.mark.django_db +def test_ingest_document_source_package_without_docx_raises(user, monkeypatch): + processing = _processing_with_file(user, "source.zip", b"zip", InputType.SOURCE_PACKAGE) + monkeypatch.setattr( + "manuscripts.utils.ingestion.ingest_zip", + lambda proc: (None, []), + ) + + with pytest.raises(ValueError, match="Documento não encontrado"): + ingest_document(processing) + + +@pytest.mark.django_db +def test_ingest_zip_source_package_requires_single_document(user, tmp_path): + zip_path = tmp_path / "multi-doc.zip" + _write_zip( + zip_path, + [("a.docx", b"a"), ("b.docx", b"b"), ("article.xml", MINIMAL_XML)], + ) + processing = _processing_with_file( + user, + "multi-doc.zip", + zip_path.read_bytes(), + InputType.SOURCE_PACKAGE, + ) + + with pytest.raises(ValueError, match="exatamente um documento"): + ingest_zip(processing) + + +@pytest.mark.django_db +def test_ingest_zip_processes_xml_docx_and_assets(user, tmp_path, monkeypatch): + zip_path = tmp_path / "sps.zip" + _write_zip( + zip_path, + [ + ("article.xml", MINIMAL_XML), + ("figures/fig1.png", b"png"), + ("notes.txt", b"other"), + ], + ) + processing = _processing_with_file( + user, + "sps.zip", + zip_path.read_bytes(), + InputType.SPS_PACKAGE, + ) + resolved = [] + monkeypatch.setattr( + "manuscripts.utils.ingestion.resolve_article_assets", + lambda proc, article, refs: resolved.append((article.pk, refs)), + ) + monkeypatch.setattr( + "manuscripts.utils.ingestion.parse_xml_structure", + lambda content: ([], [], [], []), + ) + + source_document, xml_artifacts = ingest_zip(processing) + + assert source_document is None + assert len(xml_artifacts) == 1 + article, xml_artifact = xml_artifacts[0] + assert article.pk is not None + assert xml_artifact.artifact_type == ArtifactType.XML + assert resolved == [(article.pk, [])] + assets = ArticleArtifact.objects.filter( + processing=processing, artifact_type=ArtifactType.ASSET + ) + assert assets.count() == 2 + assert {asset.original_path for asset in assets} == {"figures/fig1.png", "notes.txt"} + + +@pytest.mark.django_db +def test_ingest_zip_with_docx_sets_source_document(user, tmp_path, monkeypatch): + zip_path = tmp_path / "package.zip" + _write_zip( + zip_path, + [("draft.docx", b"docx"), ("article.xml", MINIMAL_XML)], + ) + processing = _processing_with_file( + user, + "package.zip", + zip_path.read_bytes(), + InputType.SPS_PACKAGE, + ) + monkeypatch.setattr( + "manuscripts.utils.ingestion.parse_xml_structure", + lambda content: ([], [], [], []), + ) + monkeypatch.setattr( + "manuscripts.utils.ingestion.resolve_article_assets", + lambda *args, **kwargs: None, + ) + + source_document, xml_artifacts = ingest_zip(processing) + + assert source_document.artifact_type == ArtifactType.SOURCE_DOCUMENT + assert source_document.file.read() == b"docx" + assert len(xml_artifacts) == 1 diff --git a/manuscripts/tests/test_inspection.py b/manuscripts/tests/test_inspection.py new file mode 100644 index 0000000..9f4ffae --- /dev/null +++ b/manuscripts/tests/test_inspection.py @@ -0,0 +1,251 @@ +import zipfile + +import pytest +from django.core.files.uploadedfile import SimpleUploadedFile + +from manuscripts.choices import ArtifactType, InputType, ProcessingAction, ProcessStatus +from manuscripts.models.processing import Processing +from manuscripts.utils.inspection import ( + inspect_input, + inspect_processing, + inspect_zip, + resolve_actions, + suggested_actions, +) + +MINIMAL_XML = b"<article></article>" +METADATA_XML = b"""<article xmlns:xlink="http://www.w3.org/1999/xlink" xml:lang="en"> + <front><article-meta> + <title-group><article-title>Sample title</article-title></title-group> + <article-id pub-id-type="doi">10.1234/sample</article-id> + </article-meta></front> +</article>""" + + +def _write_zip(path, members): + with zipfile.ZipFile(path, "w") as archive: + for name, data in members: + archive.writestr(name, data) + + +@pytest.mark.parametrize( + "input_type,expected", + [ + ( + InputType.DOCUMENT, + [ + ProcessingAction.CITATION_MARKUP, + ProcessingAction.XML_GENERATION, + ProcessingAction.XML_VALIDATION, + ProcessingAction.SPS_PACKAGE_GENERATION, + ProcessingAction.HTML_GENERATION, + ProcessingAction.PDF_GENERATION, + ], + ), + ( + InputType.SOURCE_PACKAGE, + [ + ProcessingAction.CITATION_MARKUP, + ProcessingAction.XML_GENERATION, + ProcessingAction.XML_VALIDATION, + ProcessingAction.SPS_PACKAGE_GENERATION, + ProcessingAction.HTML_GENERATION, + ProcessingAction.PDF_GENERATION, + ], + ), + ( + InputType.XML, + [ + ProcessingAction.XML_VALIDATION, + ProcessingAction.SPS_PACKAGE_GENERATION, + ProcessingAction.HTML_GENERATION, + ProcessingAction.PDF_GENERATION, + ], + ), + ( + InputType.SPS_PACKAGE, + [ + ProcessingAction.SPS_PACKAGE_VALIDATION, + ProcessingAction.XML_VALIDATION, + ], + ), + (InputType.UNKNOWN, []), + (InputType.AMBIGUOUS_ZIP, []), + ], +) +def test_suggested_actions_for_all_input_types(input_type, expected): + assert suggested_actions(input_type) == expected + + +def test_inspect_input_detects_docx_file(tmp_path): + docx_path = tmp_path / "manuscript.docx" + docx_path.write_bytes(b"docx") + + result = inspect_input(str(docx_path)) + + assert result["detected_type"] == InputType.DOCUMENT + assert result["contents"] == [{"path": "manuscript.docx", "kind": "document"}] + assert ProcessingAction.CITATION_MARKUP in result["suggested_actions"] + + +def test_inspect_input_detects_xml_file(tmp_path): + xml_path = tmp_path / "article.xml" + xml_path.write_text("<article></article>", encoding="utf-8") + + result = inspect_input(str(xml_path)) + + assert result["detected_type"] == InputType.XML + assert result["contents"] == [{"path": "article.xml", "kind": "xml"}] + + +def test_inspect_input_detects_zip_via_inspect_zip(tmp_path): + zip_path = tmp_path / "package.zip" + _write_zip(zip_path, [("article.xml", MINIMAL_XML)]) + + result = inspect_input(str(zip_path)) + + assert result["detected_type"] == InputType.SPS_PACKAGE + assert result["contents"][0]["kind"] == "xml" + + +def test_inspect_input_returns_unknown_for_unsupported_extension(tmp_path): + raw_path = tmp_path / "article.txt" + raw_path.write_text("hello", encoding="utf-8") + + result = inspect_input(str(raw_path)) + + assert result["detected_type"] == InputType.UNKNOWN + assert result["contents"] == [] + assert result["suggested_actions"] == [] + + +def test_inspect_zip_classifies_members_and_detects_source_package(tmp_path): + zip_path = tmp_path / "source.zip" + with zipfile.ZipFile(zip_path, "w") as archive: + archive.writestr("nested/", b"") + archive.writestr("article.docx", b"docx") + archive.writestr("assets/photo.jpg", b"jpg") + archive.writestr("readme.txt", b"txt") + + detected, contents = inspect_zip(str(zip_path)) + + assert detected == InputType.SOURCE_PACKAGE + kinds = {item["path"]: item["kind"] for item in contents} + assert kinds["article.docx"] == "document" + assert kinds["assets/photo.jpg"] == "asset" + assert kinds["readme.txt"] == "other" + assert "nested/" not in kinds + + +def test_inspect_zip_detects_sps_package(tmp_path): + zip_path = tmp_path / "sps.zip" + _write_zip(zip_path, [("article.xml", MINIMAL_XML), ("fig1.png", b"png")]) + + detected, contents = inspect_zip(str(zip_path)) + + assert detected == InputType.SPS_PACKAGE + assert {item["kind"] for item in contents} == {"xml", "asset"} + + +def test_inspect_zip_detects_ambiguous_zip(tmp_path): + zip_path = tmp_path / "ambiguous.zip" + _write_zip( + zip_path, + [("article.docx", b"docx"), ("article.xml", MINIMAL_XML)], + ) + + detected, _contents = inspect_zip(str(zip_path)) + + assert detected == InputType.AMBIGUOUS_ZIP + + +def test_resolve_actions_adds_dependencies_for_document_pipeline(): + actions = resolve_actions([ProcessingAction.XML_GENERATION], InputType.DOCUMENT) + + assert actions == [ + ProcessingAction.CITATION_MARKUP, + ProcessingAction.XML_GENERATION, + ] + + +def test_resolve_actions_allows_html_and_pdf_for_sps_package(): + actions = resolve_actions( + [ProcessingAction.HTML_GENERATION, ProcessingAction.PDF_GENERATION], + InputType.SPS_PACKAGE, + ) + + assert actions == [ + ProcessingAction.XML_VALIDATION, + ProcessingAction.HTML_GENERATION, + ProcessingAction.PDF_GENERATION, + ] + + +def test_resolve_actions_ignores_invalid_actions(): + actions = resolve_actions(["not-real-action"], InputType.XML) + assert actions == [] + + +@pytest.mark.django_db +def test_inspect_processing_for_xml_file(user): + uploaded = SimpleUploadedFile("article.xml", METADATA_XML, content_type="application/xml") + processing = Processing.objects.create( + title="XML inspection", + creator=user, + input_file=uploaded, + ) + + result = inspect_processing(processing) + + processing.refresh_from_db() + assert result.pk == processing.pk + assert processing.detected_type == InputType.XML + assert processing.status == ProcessStatus.AWAITING_REVIEW + assert processing.requested_actions == suggested_actions(InputType.XML) + assert processing.input_checksum + assert processing.inspection["article_candidates"][0]["title"] == "Sample title" + assert processing.artifacts.filter(artifact_type=ArtifactType.INPUT).exists() + + +@pytest.mark.django_db +def test_inspect_processing_for_sps_zip(user, tmp_path): + zip_path = tmp_path / "sps.zip" + _write_zip(zip_path, [("articles/sample.xml", METADATA_XML)]) + uploaded = SimpleUploadedFile("sps.zip", zip_path.read_bytes(), content_type="application/zip") + processing = Processing.objects.create( + title="SPS inspection", + creator=user, + input_file=uploaded, + ) + + inspect_processing(processing) + + processing.refresh_from_db() + assert processing.detected_type == InputType.SPS_PACKAGE + assert len(processing.inspection["article_candidates"]) == 1 + assert processing.inspection["article_candidates"][0]["path"] == "articles/sample.xml" + assert processing.inspection["article_candidates"][0]["title"] == "Sample title" + + +@pytest.mark.django_db +def test_inspect_processing_records_warning_on_metadata_failure(user, monkeypatch): + uploaded = SimpleUploadedFile("article.xml", METADATA_XML, content_type="application/xml") + processing = Processing.objects.create( + title="XML warning", + creator=user, + input_file=uploaded, + ) + + def broken_metadata(_content): + raise ValueError("invalid metadata") + + monkeypatch.setattr( + "manuscripts.utils.inspection.extract_article_metadata", + broken_metadata, + ) + + inspect_processing(processing) + + processing.refresh_from_db() + assert processing.inspection["inspection_warning"] == "invalid metadata" + assert processing.inspection["article_candidates"] == [] diff --git a/manuscripts/tests/test_models.py b/manuscripts/tests/test_models.py new file mode 100644 index 0000000..d112c13 --- /dev/null +++ b/manuscripts/tests/test_models.py @@ -0,0 +1,160 @@ +import pytest + +from manuscripts.choices import ArtifactType, EventStatus, InputType, ProcessingAction +from manuscripts.models.article import ( + Article, + ArticleArtifact, + ArticleReference, + ArticleStructureVersion, + CitationOccurrence, +) +from manuscripts.models.processing import ( + ArticleInput, + Processing, + ProcessingEvent, + SPSPackageImport, + XMLImport, +) + + +@pytest.mark.django_db +def test_processing_article_count_reflects_related_articles(processing, article): + assert processing.article_count == 0 + processing.articles.add(article) + assert processing.article_count == 1 + + +@pytest.mark.django_db +def test_processing_str_uses_title_when_available(processing): + assert str(processing) == "Happy path processing" + + +@pytest.mark.django_db +def test_proxy_managers_filter_by_detected_or_confirmed_type(user): + doc_processing = Processing.objects.create( + title="doc", + creator=user, + confirmed_type=InputType.DOCUMENT, + input_file="processings/input/doc.docx", + ) + xml_processing = Processing.objects.create( + title="xml", + creator=user, + detected_type=InputType.XML, + input_file="processings/input/input.xml", + ) + + assert ArticleInput.objects.filter(pk=doc_processing.pk).exists() + assert not ArticleInput.objects.filter(pk=xml_processing.pk).exists() + assert XMLImport.objects.filter(pk=xml_processing.pk).exists() + + +@pytest.mark.django_db +def test_article_str_prefers_title(article): + assert str(article) == "Happy path article" + + +@pytest.mark.django_db +def test_article_str_falls_back_to_doi(user): + article = Article.objects.create(doi="10.0000/fallback", creator=user) + assert str(article) == "10.0000/fallback" + + +@pytest.mark.django_db +def test_processing_event_str_uses_action_label(processing): + event = ProcessingEvent.objects.create( + processing=processing, + action=ProcessingAction.XML_VALIDATION, + status=EventStatus.PENDING, + ) + + assert "Validate XML" in str(event) + + +@pytest.mark.django_db +def test_article_artifact_str_includes_type_and_version(processing, article): + artifact = ArticleArtifact.objects.create( + processing=processing, + article=article, + artifact_type=ArtifactType.XML, + version=2, + file="processings/artifacts/sample.xml", + ) + + assert str(artifact) == "XML SPS v2" + + +@pytest.mark.django_db +def test_article_current_artifact_and_structure(processing, article): + from manuscripts.artifacts import save_artifact + from manuscripts.structure import create_structure_version + + assert article.current_artifact(ArtifactType.XML) is None + assert article.current_structure is None + + structure = create_structure_version(article, processing, InputType.XML, [], [], []) + artifact = save_artifact( + processing, + ArtifactType.XML, + "article.xml", + b"<article />", + article=article, + structure=structure, + ) + + assert article.current_artifact(ArtifactType.XML) == artifact + assert article.current_structure == structure + + +@pytest.mark.django_db +def test_article_structure_version_str(processing, article): + from manuscripts.structure import create_structure_version + + structure = create_structure_version(article, processing, InputType.XML, [], [], []) + assert str(structure) == f"{article} - structure v1" + + +@pytest.mark.django_db +def test_article_reference_and_citation_str(processing, article, user): + from manuscripts.structure import create_structure_version + + structure = create_structure_version( + article, + processing, + InputType.XML, + [], + [], + [{"type": "ref_paragraph", "value": {"refid": "B1", "paragraph": "Citation text"}}], + ) + reference = structure.references.first() + assert str(reference).startswith("B1:") + + citation = CitationOccurrence.objects.create( + structure=structure, + text="(Author, 2024)", + status=CitationOccurrence.Status.ORPHAN, + creator=user, + ) + assert str(citation) == "(Author, 2024)" + + +@pytest.mark.django_db +def test_sps_package_import_manager_filters_ambiguous_zip(user): + ambiguous = Processing.objects.create( + title="ambiguous", + creator=user, + detected_type=InputType.AMBIGUOUS_ZIP, + input_file="processings/input/ambiguous.zip", + ) + + assert SPSPackageImport.objects.filter(pk=ambiguous.pk).exists() + + +@pytest.mark.django_db +def test_processing_event_str_without_action(processing): + event = ProcessingEvent.objects.create( + processing=processing, + status=EventStatus.PENDING, + ) + + assert "Input" in str(event) diff --git a/manuscripts/tests/test_processing.py b/manuscripts/tests/test_processing.py new file mode 100644 index 0000000..aa8adaa --- /dev/null +++ b/manuscripts/tests/test_processing.py @@ -0,0 +1,392 @@ +import pytest +from types import SimpleNamespace + +from django.utils import timezone + +from manuscripts.choices import ( + ArtifactType, + EventStatus, + InputType, + ProcessingAction, + ProcessStatus, +) +from manuscripts.models.processing import Processing, ProcessingEvent +from manuscripts.processing import ( + _begin_processing, + _complete_processing, + _execute_actions, + _fail_processing, + _ingest_input, + _record_skipped_actions, + _resolve_pipeline_actions, + process_input, +) + +MINIMAL_XML = b"<article></article>" + + +@pytest.mark.django_db +def test_begin_processing_resets_error_fields_and_sets_processing(processing): + processing.status = ProcessStatus.FAILED + processing.error_message = "old error" + processing.error_details = {"action": "old"} + processing.error_traceback = "traceback" + processing.completed_at = timezone.now() + processing.save() + + result = _begin_processing(processing.pk) + + result.refresh_from_db() + assert result.status == ProcessStatus.PROCESSING + assert result.processing_started_at is not None + assert result.completed_at is None + assert result.error_message == "" + assert result.error_details == {} + assert result.error_traceback == "" + + +@pytest.mark.django_db +def test_ingest_input_rejects_unknown_confirmed_type(processing): + processing.confirmed_type = InputType.UNKNOWN + processing.save(update_fields=["confirmed_type"]) + + with pytest.raises(ValueError, match="Confirme o tipo de entrada"): + _ingest_input(processing) + + +@pytest.mark.django_db +def test_ingest_input_rejects_unprocessable_confirmed_type(processing): + processing.confirmed_type = InputType.AMBIGUOUS_ZIP + processing.save(update_fields=["confirmed_type"]) + + with pytest.raises(ValueError, match="não processável"): + _ingest_input(processing) + + +@pytest.mark.django_db +def test_ingest_input_document_path(processing, article, monkeypatch): + processing.confirmed_type = InputType.DOCUMENT + processing.save(update_fields=["confirmed_type"]) + source = SimpleNamespace(file=SimpleNamespace(name="article.docx")) + calls = {"extract": 0} + + monkeypatch.setattr( + "manuscripts.processing.ingest_document", + lambda proc: source if proc.pk == processing.pk else None, + ) + monkeypatch.setattr( + "manuscripts.processing.get_or_create_article", + lambda proc, title: (article, {}) if proc.pk == processing.pk else None, + ) + + def fake_extract(proc, src, art): + calls["extract"] += 1 + assert proc.pk == processing.pk + assert src is source + assert art.pk == article.pk + + monkeypatch.setattr("manuscripts.processing.extract_docx_assets", fake_extract) + + _ingest_input(processing) + + assert calls["extract"] == 1 + + +@pytest.mark.django_db +def test_ingest_input_source_package_path(processing, article, monkeypatch): + processing.confirmed_type = InputType.SOURCE_PACKAGE + processing.save(update_fields=["confirmed_type"]) + source = SimpleNamespace(file=SimpleNamespace(name="package/article.docx")) + calls = {"extract": 0} + + monkeypatch.setattr("manuscripts.processing.ingest_document", lambda proc: source) + monkeypatch.setattr( + "manuscripts.processing.get_or_create_article", + lambda proc, title: (article, {}), + ) + monkeypatch.setattr( + "manuscripts.processing.extract_docx_assets", + lambda proc, src, art: calls.update(extract=calls["extract"] + 1), + ) + + _ingest_input(processing) + + assert calls["extract"] == 1 + + +@pytest.mark.django_db +def test_ingest_input_xml_path(processing, monkeypatch): + processing.confirmed_type = InputType.XML + processing.save(update_fields=["confirmed_type"]) + article = SimpleNamespace(pk=1) + xml_artifact = SimpleNamespace(metadata={"referenced_assets": ["fig1.png"]}) + resolved = [] + + monkeypatch.setattr( + "manuscripts.processing.ingest_xml", + lambda proc, name, content: (article, xml_artifact), + ) + monkeypatch.setattr( + "manuscripts.processing.resolve_article_assets", + lambda proc, art, refs: resolved.append((proc.pk, art, refs)), + ) + + _ingest_input(processing) + + assert resolved == [(processing.pk, article, ["fig1.png"])] + + +@pytest.mark.django_db +def test_ingest_input_sps_package_path(processing, monkeypatch): + processing.confirmed_type = InputType.SPS_PACKAGE + processing.save(update_fields=["confirmed_type"]) + ingested = [] + + monkeypatch.setattr( + "manuscripts.processing.ingest_zip", + lambda proc: ingested.append(proc.pk) or ("source", []), + ) + + _ingest_input(processing) + + assert ingested == [processing.pk] + + +@pytest.mark.django_db +def test_resolve_pipeline_actions_without_start_action(processing): + processing.confirmed_type = InputType.XML + processing.requested_actions = [ProcessingAction.XML_VALIDATION] + processing.save(update_fields=["confirmed_type", "requested_actions"]) + + actions = _resolve_pipeline_actions(processing, start_action=None) + + assert actions == [ProcessingAction.XML_VALIDATION] + + +@pytest.mark.django_db +def test_resolve_pipeline_actions_with_start_action(processing): + processing.confirmed_type = InputType.DOCUMENT + processing.requested_actions = [ProcessingAction.XML_GENERATION] + processing.save(update_fields=["confirmed_type", "requested_actions"]) + + actions = _resolve_pipeline_actions(processing, start_action=ProcessingAction.XML_VALIDATION) + + assert actions == [ + ProcessingAction.XML_VALIDATION, + ProcessingAction.SPS_PACKAGE_GENERATION, + ProcessingAction.HTML_GENERATION, + ProcessingAction.PDF_GENERATION, + ] + + +@pytest.mark.django_db +def test_resolve_pipeline_actions_rejects_invalid_start_action(processing): + processing.confirmed_type = InputType.XML + processing.save(update_fields=["confirmed_type"]) + + with pytest.raises(ValueError, match="não se aplica"): + _resolve_pipeline_actions(processing, start_action=ProcessingAction.CITATION_MARKUP) + + +@pytest.mark.django_db +def test_resolve_pipeline_actions_invalidates_stale_artifacts(processing, article): + processing.confirmed_type = InputType.DOCUMENT + processing.requested_actions = [ProcessingAction.XML_GENERATION] + processing.save(update_fields=["confirmed_type", "requested_actions"]) + from manuscripts.artifacts import save_artifact + + artifact = save_artifact( + processing, + ArtifactType.HTML, + "article.html", + b"<html></html>", + article=article, + ) + assert artifact.is_current is True + assert artifact.is_stale is False + + _resolve_pipeline_actions(processing, start_action=ProcessingAction.XML_VALIDATION) + + artifact.refresh_from_db() + assert artifact.is_current is False + assert artifact.is_stale is True + + +@pytest.mark.django_db +def test_record_skipped_actions_creates_events_for_unselected_actions(processing): + processing.confirmed_type = InputType.XML + processing.save(update_fields=["confirmed_type"]) + selected = [ProcessingAction.XML_VALIDATION] + + _record_skipped_actions(processing, selected) + + skipped = { + event.action + for event in ProcessingEvent.objects.filter( + processing=processing, status=EventStatus.SKIPPED + ) + } + assert ProcessingAction.SPS_PACKAGE_GENERATION in skipped + assert ProcessingAction.XML_VALIDATION not in skipped + + +@pytest.mark.django_db +def test_execute_actions_without_task_instance(processing, monkeypatch): + calls = [] + + monkeypatch.setattr( + "manuscripts.processing.run_action", + lambda proc, action, task_id: calls.append((action, task_id)) or False, + ) + + partial = _execute_actions( + processing, + [ProcessingAction.XML_VALIDATION, ProcessingAction.HTML_GENERATION], + task_instance=None, + ) + + assert partial is False + assert calls == [ + (ProcessingAction.XML_VALIDATION, None), + (ProcessingAction.HTML_GENERATION, None), + ] + + +@pytest.mark.django_db +def test_execute_actions_with_task_instance(processing, monkeypatch): + task_instance = SimpleNamespace(request=SimpleNamespace(id="celery-task-42")) + calls = [] + + monkeypatch.setattr( + "manuscripts.processing.run_action", + lambda proc, action, task_id: calls.append(task_id) or (action == ProcessingAction.XML_VALIDATION), + ) + + partial = _execute_actions(processing, [ProcessingAction.XML_VALIDATION], task_instance) + + assert partial is True + assert calls == ["celery-task-42"] + + +@pytest.mark.django_db +def test_complete_processing_marks_completed(processing, article): + processing.articles.add(article) + processing.current_action = ProcessingAction.XML_VALIDATION + processing.save(update_fields=["current_action"]) + + _complete_processing(processing, partial=False) + + processing.refresh_from_db() + article.refresh_from_db() + assert processing.status == ProcessStatus.COMPLETED + assert processing.completed_at is not None + assert processing.current_action == "" + assert article.status == ProcessStatus.COMPLETED + + +@pytest.mark.django_db +def test_complete_processing_marks_partial_from_flag(processing, article): + processing.articles.add(article) + + _complete_processing(processing, partial=True) + + processing.refresh_from_db() + article.refresh_from_db() + assert processing.status == ProcessStatus.PARTIAL + assert article.status == ProcessStatus.PARTIAL + + +@pytest.mark.django_db +def test_complete_processing_marks_partial_from_warning_event(processing, article): + processing.articles.add(article) + ProcessingEvent.objects.create( + processing=processing, + article=article, + status=EventStatus.FAILED, + details={"severity": "warning"}, + ) + + _complete_processing(processing, partial=False) + + processing.refresh_from_db() + assert processing.status == ProcessStatus.PARTIAL + + +@pytest.mark.django_db +def test_fail_processing_records_error_and_fails_running_events(processing): + processing.current_action = ProcessingAction.XML_VALIDATION + processing.save(update_fields=["current_action"]) + running = ProcessingEvent.objects.create( + processing=processing, + action=ProcessingAction.XML_VALIDATION, + status=EventStatus.RUNNING, + ) + + with pytest.raises(RuntimeError, match="boom"): + try: + raise RuntimeError("boom") + except RuntimeError as exc: + _fail_processing(processing, exc) + raise + + processing.refresh_from_db() + running.refresh_from_db() + assert processing.status == ProcessStatus.FAILED + assert processing.error_message == "boom" + assert processing.error_details["type"] == "RuntimeError" + assert processing.error_details["action"] == ProcessingAction.XML_VALIDATION + assert processing.error_traceback + assert running.status == EventStatus.FAILED + assert running.message == "boom" + assert running.completed_at is not None + + +@pytest.mark.django_db +def test_process_input_success_path(processing, monkeypatch): + processing.confirmed_type = InputType.XML + processing.save(update_fields=["confirmed_type"]) + pipeline = [] + + monkeypatch.setattr("manuscripts.processing._ingest_input", lambda proc: pipeline.append("ingest")) + monkeypatch.setattr( + "manuscripts.processing._resolve_pipeline_actions", + lambda proc, start: pipeline.append(("resolve", start)) or [ProcessingAction.XML_VALIDATION], + ) + monkeypatch.setattr( + "manuscripts.processing._record_skipped_actions", + lambda proc, actions: pipeline.append(("skipped", actions)), + ) + monkeypatch.setattr( + "manuscripts.processing._execute_actions", + lambda proc, actions, task: pipeline.append(("execute", task)) or False, + ) + monkeypatch.setattr( + "manuscripts.processing._complete_processing", + lambda proc, partial: pipeline.append(("complete", partial)), + ) + task = SimpleNamespace(request=SimpleNamespace(id="task-1")) + + process_input(task, processing.pk, start_action=ProcessingAction.XML_VALIDATION) + + assert pipeline[0] == "ingest" + assert pipeline[1] == ("resolve", ProcessingAction.XML_VALIDATION) + processing.refresh_from_db() + assert processing.status == ProcessStatus.PROCESSING + + +@pytest.mark.django_db +def test_process_input_failure_path(processing, monkeypatch): + processing.confirmed_type = InputType.XML + processing.save(update_fields=["confirmed_type"]) + + def fail_ingest(_proc): + raise ValueError("ingest failed") + + monkeypatch.setattr("manuscripts.processing._ingest_input", fail_ingest) + + with pytest.raises(ValueError, match="ingest failed"): + process_input(None, processing.pk) + + processing.refresh_from_db() + assert processing.status == ProcessStatus.FAILED + assert processing.error_message == "ingest failed" diff --git a/manuscripts/tests/test_processing_actions.py b/manuscripts/tests/test_processing_actions.py new file mode 100644 index 0000000..0fd9d1e --- /dev/null +++ b/manuscripts/tests/test_processing_actions.py @@ -0,0 +1,550 @@ +import io +import os +import zipfile +from types import SimpleNamespace + +import pytest + +from manuscripts.artifacts import save_artifact, save_path_artifact +from manuscripts.choices import ArtifactType, EventStatus, InputType, ProcessingAction +from manuscripts.models.article import ArticleArtifact +from manuscripts.models.processing import ProcessingEvent +from manuscripts.tests.test_structure import _make_structure, _paragraph_block +from manuscripts.utils.processing_actions import ( + _ensure_article_title, + _unwrap_item, + run_action, +) + + +class FakeDocx: + def __init__(self, payload=b"docx-bytes"): + self._payload = payload + self.save_calls = 0 + + def save(self, buffer): + self.save_calls += 1 + buffer.write(self._payload) + + +@pytest.fixture +def linked_processing(processing, article): + processing.articles.add(article) + processing.confirmed_type = InputType.DOCUMENT + processing.save(update_fields=["confirmed_type"]) + return processing + + +@pytest.fixture +def source_document(linked_processing, article, tmp_path): + docx_path = tmp_path / "article.docx" + docx_path.write_bytes(b"source-docx") + return save_path_artifact( + linked_processing, + ArtifactType.SOURCE_DOCUMENT, + str(docx_path), + article=article, + ) + + +@pytest.fixture +def structure(linked_processing, article): + return _make_structure(article, linked_processing, body=[_paragraph_block("Body text.")]) + + +@pytest.fixture +def xml_artifact(linked_processing, article, structure, tmp_path): + xml_path = tmp_path / "article.xml" + xml_path.write_bytes(b'<article><front><article-meta/></front></article>') + return save_path_artifact( + linked_processing, + ArtifactType.XML, + str(xml_path), + article=article, + structure=structure, + ) + + +def _mock_docx_parser(monkeypatch, document): + monkeypatch.setattr( + "manuscripts.utils.processing_actions.DocxParser.open_docx", + lambda _path: document, + ) + + +def _mock_citation_externals( + monkeypatch, + *, + marked=True, + validation=None, + front=None, + body=None, + back=None, + xref_status=None, + article_updates=None, + frontmatter_warnings=None, + frontmatter_ai=None, +): + document = FakeDocx() + marked_calls = {"mark": 0} + + def fake_is_marked(_doc): + return marked + + def fake_mark_references(doc): + marked_calls["mark"] += 1 + return doc + + monkeypatch.setattr("manuscripts.utils.processing_actions.is_marked", fake_is_marked) + monkeypatch.setattr("manuscripts.utils.processing_actions.mark_references", fake_mark_references) + monkeypatch.setattr( + "manuscripts.utils.processing_actions.validate_marks", + lambda _doc: validation or {"valid": True, "issues": 0}, + ) + monkeypatch.setattr( + "manuscripts.utils.processing_actions.extract_docx_structure", + lambda _doc, _path: ( + front if front is not None else [], + body if body is not None else [_paragraph_block("Body.")], + back if back is not None else [], + xref_status if xref_status is not None else {}, + ), + ) + monkeypatch.setattr( + "manuscripts.utils.processing_actions.extract_frontmatter", + lambda _article, _front, _body, docx_path=None: ( + front if front is not None else [], + article_updates or {}, + frontmatter_warnings or [], + frontmatter_ai or {"status": "completed", "provider": "test"}, + ), + ) + _mock_docx_parser(monkeypatch, document) + return document, marked_calls + + +def _mock_structure_sync(monkeypatch): + monkeypatch.setattr("manuscripts.structure.get_reference.delay", lambda _pk: None) + monkeypatch.setattr("manuscripts.structure.transaction.on_commit", lambda callback: callback()) + + +@pytest.mark.django_db +@pytest.mark.parametrize( + ("rows", "exceptions", "expected"), + [ + ([{"line": 1}], [], True), + ([], [{"type": "error"}], True), + ([], [], False), + ], +) +def test_run_action_sps_package_validation(processing, monkeypatch, rows, exceptions, expected): + monkeypatch.setattr( + "manuscripts.utils.processing_actions.xml_utils.validate_zip", + lambda _path: (rows, exceptions), + ) + + result = run_action(processing, ProcessingAction.SPS_PACKAGE_VALIDATION, "task-sps") + + assert result is expected + event = ProcessingEvent.objects.get( + processing=processing, + action=ProcessingAction.SPS_PACKAGE_VALIDATION, + ) + assert event.status == EventStatus.COMPLETED + assert event.details == {"issues": len(rows), "exceptions": len(exceptions)} + assert ArticleArtifact.objects.filter( + processing=processing, + artifact_type=ArtifactType.VALIDATION_REPORT, + is_current=True, + ).exists() + assert ArticleArtifact.objects.filter( + processing=processing, + artifact_type=ArtifactType.VALIDATION_EXCEPTIONS, + is_current=True, + ).exists() + + +@pytest.mark.django_db +def test_run_action_citation_markup_unmarked_document( + linked_processing, article, source_document, monkeypatch +): + _mock_structure_sync(monkeypatch) + article.title = "" + article.save(update_fields=["title"]) + front = [ + { + "type": "paragraph_with_language", + "value": {"label": "<article-title>", "paragraph": "Derived title"}, + } + ] + document, marked_calls = _mock_citation_externals( + monkeypatch, + marked=False, + front=front, + article_updates={"doi": "10.0000/updated"}, + frontmatter_warnings=["warn-one"], + frontmatter_ai={"status": "completed", "provider": "ollama"}, + ) + + result = run_action(linked_processing, ProcessingAction.CITATION_MARKUP, "task-cite") + + assert result is False + assert marked_calls["mark"] == 1 + assert document.save_calls == 1 + article.refresh_from_db() + assert article.doi == "10.0000/updated" + assert article.title == "Derived title" + marked = ArticleArtifact.objects.get( + processing=linked_processing, + article=article, + artifact_type=ArtifactType.MARKED_DOCUMENT, + is_current=True, + ) + assert marked.metadata == {"valid": True, "issues": 0} + event = ProcessingEvent.objects.get( + processing=linked_processing, + article=article, + action=ProcessingAction.CITATION_MARKUP, + ) + assert event.status == EventStatus.COMPLETED + assert event.details["structure_version"] == 1 + assert event.details["body_blocks"] == 1 + assert event.details["frontmatter_ai_warnings"] == ["warn-one"] + + +@pytest.mark.django_db +def test_run_action_citation_markup_marked_document_skips_mark_references( + linked_processing, article, source_document, monkeypatch +): + _mock_structure_sync(monkeypatch) + article.title = "Preset title" + article.save(update_fields=["title"]) + _document, marked_calls = _mock_citation_externals(monkeypatch, marked=True, article_updates={}) + + run_action(linked_processing, ProcessingAction.CITATION_MARKUP, "task-cite") + + assert marked_calls["mark"] == 0 + article.refresh_from_db() + assert article.title == "Preset title" + + +@pytest.mark.django_db +def test_run_action_xml_generation_without_structure_raises(linked_processing, article): + linked_processing.articles.add(article) + + with pytest.raises(ValueError, match="não possui estrutura"): + run_action(linked_processing, ProcessingAction.XML_GENERATION, "task-xml") + + +@pytest.mark.django_db +def test_run_action_xml_generation_creates_xml( + linked_processing, article, structure, monkeypatch +): + monkeypatch.setattr( + "manuscripts.utils.processing_actions.generate_structure_xml", + lambda _structure: b"<article generated='true' />", + ) + + run_action(linked_processing, ProcessingAction.XML_GENERATION, "task-xml") + + artifact = ArticleArtifact.objects.get( + processing=linked_processing, + article=article, + artifact_type=ArtifactType.XML, + is_current=True, + ) + assert artifact.file.read() == b"<article generated='true' />" + event = ProcessingEvent.objects.get( + processing=linked_processing, + article=article, + action=ProcessingAction.XML_GENERATION, + ) + assert event.status == EventStatus.COMPLETED + + +@pytest.mark.django_db +@pytest.mark.parametrize( + ("validation_content", "exceptions_content", "expected_partial"), + [ + ("header\nissue\n", "", True), + ("header\n", "exception details", True), + ("header\n", " \n", False), + ], +) +def test_run_action_xml_validation_issues( + linked_processing, + article, + xml_artifact, + tmp_path, + monkeypatch, + validation_content, + exceptions_content, + expected_partial, +): + validation_path = tmp_path / "validation.tsv" + exceptions_path = tmp_path / "exceptions.txt" + validation_path.write_text(validation_content, encoding="utf-8") + exceptions_path.write_text(exceptions_content, encoding="utf-8") + monkeypatch.setattr( + "manuscripts.utils.processing_actions.xml_utils.validate_xml_document", + lambda _xml_path, _output_dir, _config: (str(validation_path), str(exceptions_path)), + ) + + result = run_action(linked_processing, ProcessingAction.XML_VALIDATION, "task-validate") + + assert result is expected_partial + event = ProcessingEvent.objects.get( + processing=linked_processing, + article=article, + action=ProcessingAction.XML_VALIDATION, + ) + assert event.details["has_issues"] is expected_partial + + +@pytest.mark.django_db +def test_run_action_sps_package_generation_includes_referenced_assets_only( + linked_processing, article, xml_artifact, tmp_path, monkeypatch +): + monkeypatch.setattr( + "manuscripts.utils.processing_actions.extract_article_metadata", + lambda _xml: {"assets": ["media/fig1.png?v=1", "fig2.png"]}, + ) + save_artifact( + linked_processing, + ArtifactType.ASSET, + "fig1.png", + b"png-one", + article=article, + original_path="media/fig1.png", + ) + save_artifact( + linked_processing, + ArtifactType.ASSET, + "ignored.png", + b"ignored", + article=article, + original_path="media/ignored.png", + ) + + run_action(linked_processing, ProcessingAction.SPS_PACKAGE_GENERATION, "task-sps-gen") + + package = ArticleArtifact.objects.get( + processing=linked_processing, + article=article, + artifact_type=ArtifactType.SPS_PACKAGE, + is_current=True, + ) + with zipfile.ZipFile(io.BytesIO(package.file.read())) as archive: + names = set(archive.namelist()) + assert os.path.basename(xml_artifact.file.name) in names + assert "media/fig1.png" in names + assert "media/ignored.png" not in names + + +@pytest.mark.django_db +def test_run_action_html_generation( + linked_processing, article, xml_artifact, tmp_path, monkeypatch +): + html_path = tmp_path / "article.html" + html_path.write_text("<html>preview</html>", encoding="utf-8") + captured = {} + + def fake_generate_html(xml_path, output_dir, config, asset_url_map=None): + captured["xml_path"] = xml_path + captured["output_dir"] = output_dir + captured["config"] = config + captured["asset_url_map"] = asset_url_map + return str(html_path), "en" + + monkeypatch.setattr( + "manuscripts.utils.processing_actions.xml_utils.generate_html_for_xml_document", + fake_generate_html, + ) + monkeypatch.setattr( + "manuscripts.utils.processing_actions.article_asset_url_map", + lambda _processing, _article: {"fig.png": "/media/fig.png"}, + ) + + run_action(linked_processing, ProcessingAction.HTML_GENERATION, "task-html") + + artifact = ArticleArtifact.objects.get( + processing=linked_processing, + article=article, + artifact_type=ArtifactType.HTML, + is_current=True, + ) + assert artifact.metadata == {"language": "en"} + assert captured["asset_url_map"] == {"fig.png": "/media/fig.png"} + + +@pytest.mark.django_db +def test_run_action_pdf_generation_without_docx_path_cleans_assets_dir( + linked_processing, article, xml_artifact, tmp_path, monkeypatch +): + assets_dir = tmp_path / "pdf-assets" + assets_dir.mkdir() + (assets_dir / "figure.png").write_bytes(b"png") + pdf_path = tmp_path / "article.pdf" + pdf_path.write_bytes(b"%PDF-1.4") + + monkeypatch.setattr( + "manuscripts.utils.processing_actions.article_assets_dir", + lambda _processing, _article: str(assets_dir), + ) + monkeypatch.setattr( + "manuscripts.utils.processing_actions.article_asset_url_map", + lambda _processing, _article: {}, + ) + monkeypatch.setattr( + "manuscripts.utils.processing_actions.xml_utils.generate_pdf_for_xml_document", + lambda _xml_path, _output_dir, _params: (str(pdf_path), None, "pt"), + ) + + run_action(linked_processing, ProcessingAction.PDF_GENERATION, "task-pdf") + + assert not assets_dir.exists() + assert ArticleArtifact.objects.filter( + processing=linked_processing, + article=article, + artifact_type=ArtifactType.PDF, + is_current=True, + ).exists() + assert not ArticleArtifact.objects.filter( + processing=linked_processing, + article=article, + artifact_type=ArtifactType.INTERMEDIATE_DOCUMENT, + ).exists() + + +@pytest.mark.django_db +def test_run_action_pdf_generation_with_docx_path( + linked_processing, article, xml_artifact, tmp_path, monkeypatch +): + assets_dir = tmp_path / "pdf-assets-with-docx" + assets_dir.mkdir() + pdf_path = tmp_path / "article.pdf" + docx_path = tmp_path / "article.docx" + pdf_path.write_bytes(b"%PDF-1.4") + docx_path.write_bytes(b"docx") + + monkeypatch.setattr( + "manuscripts.utils.processing_actions.article_assets_dir", + lambda _processing, _article: str(assets_dir), + ) + monkeypatch.setattr( + "manuscripts.utils.processing_actions.article_asset_url_map", + lambda _processing, _article: {"fig.png": "/media/fig.png"}, + ) + monkeypatch.setattr( + "manuscripts.utils.processing_actions.xml_utils.generate_pdf_for_xml_document", + lambda _xml_path, _output_dir, _params: (str(pdf_path), str(docx_path), "en"), + ) + + run_action(linked_processing, ProcessingAction.PDF_GENERATION, "task-pdf") + + assert not assets_dir.exists() + assert ArticleArtifact.objects.filter( + processing=linked_processing, + article=article, + artifact_type=ArtifactType.INTERMEDIATE_DOCUMENT, + is_current=True, + ).exists() + + +def test_unwrap_item_object_with_block_type(): + item = SimpleNamespace( + block_type="paragraph_with_language", + value={"label": "<article-title>", "paragraph": "Title"}, + ) + + assert _unwrap_item(item) == ( + "paragraph_with_language", + {"label": "<article-title>", "paragraph": "Title"}, + ) + + +def test_unwrap_item_dict(): + assert _unwrap_item({"type": "paragraph", "value": {"label": "<p>", "paragraph": "Text"}}) == ( + "paragraph", + {"label": "<p>", "paragraph": "Text"}, + ) + + +def test_unwrap_item_dict_without_value(): + assert _unwrap_item({"type": "paragraph"}) == ("paragraph", {}) + + +def test_unwrap_item_fallback(): + assert _unwrap_item("unsupported") == (None, {}) + + +@pytest.mark.django_db +def test_ensure_article_title_from_block_type_object(article): + article.title = "" + article.save(update_fields=["title"]) + front = [ + SimpleNamespace( + block_type="paragraph_with_language", + value={"label": "<article-title>", "paragraph": "Object block title"}, + ) + ] + + _ensure_article_title(article, front) + + article.refresh_from_db() + assert article.title == "Object block title" + + +@pytest.mark.django_db +def test_ensure_article_title_from_dict_front(article): + article.title = "" + article.save(update_fields=["title"]) + front = [ + { + "type": "paragraph", + "value": {"label": "<article-title>", "paragraph": "Dict block title"}, + } + ] + + _ensure_article_title(article, front) + + article.refresh_from_db() + assert article.title == "Dict block title" + + +@pytest.mark.django_db +def test_ensure_article_title_falls_back_to_first_front_block(article): + article.title = "" + article.save(update_fields=["title"]) + front = [ + {"type": "paragraph", "value": {"label": "<p>", "paragraph": "First paragraph title"}}, + {"type": "paragraph", "value": {"label": "<p>", "paragraph": "Second paragraph"}}, + ] + + _ensure_article_title(article, front) + + article.refresh_from_db() + assert article.title == "First paragraph title" + + +@pytest.mark.django_db +def test_ensure_article_title_skips_when_title_already_set(article): + article.title = "Existing title" + article.save(update_fields=["title"]) + front = [{"type": "paragraph", "value": {"label": "<article-title>", "paragraph": "Ignored"}}] + + _ensure_article_title(article, front) + + article.refresh_from_db() + assert article.title == "Existing title" + + +@pytest.mark.django_db +def test_ensure_article_title_skips_when_front_is_empty(article): + article.title = "" + article.save(update_fields=["title"]) + + _ensure_article_title(article, []) + + article.refresh_from_db() + assert article.title == "" diff --git a/manuscripts/tests/test_services.py b/manuscripts/tests/test_services.py new file mode 100644 index 0000000..4370929 --- /dev/null +++ b/manuscripts/tests/test_services.py @@ -0,0 +1,137 @@ +import pytest + +from manuscripts.artifacts import save_artifact +from manuscripts.choices import ArtifactType, EventStatus, InputType, ProcessingAction +from manuscripts.controller import event_complete, event_start, get_or_create_article +from manuscripts.models.article import Article, ArticleStructureVersion +from manuscripts.structure import create_structure_version + + +@pytest.mark.django_db +def test_get_or_create_article_creates_article_and_links_to_processing(processing, monkeypatch): + metadata = { + "title": "Imported title", + "doi": "10.1234/example.doi", + "language": "en", + } + monkeypatch.setattr("manuscripts.controller.extract_article_metadata", lambda _xml: metadata) + + article, returned_metadata = get_or_create_article( + processing=processing, + xml_content=b"<article><front /></article>", + ) + + assert article.pk is not None + assert article.title == "Imported title" + assert article.doi == "10.1234/example.doi" + assert processing.articles.filter(pk=article.pk).exists() + assert returned_metadata == metadata + + +@pytest.mark.django_db +def test_get_or_create_article_reuses_existing_article_by_doi(processing, article, monkeypatch): + monkeypatch.setattr( + "manuscripts.controller.extract_article_metadata", + lambda _xml: {"title": "Updated title", "doi": article.doi}, + ) + + reused, _metadata = get_or_create_article( + processing=processing, + xml_content=b"<article />", + ) + + assert reused.pk == article.pk + assert Article.objects.filter(doi=article.doi).count() == 1 + assert processing.articles.filter(pk=article.pk).exists() + + +@pytest.mark.django_db +def test_event_start_and_complete_track_processing_action(processing): + event = event_start( + processing=processing, + action=ProcessingAction.XML_VALIDATION, + task_id="task-123", + ) + + processing.refresh_from_db() + assert processing.current_action == ProcessingAction.XML_VALIDATION + assert event.status == EventStatus.RUNNING + assert event.task_id == "task-123" + + event_complete(event, message="done", details={"ok": True}) + + event.refresh_from_db() + assert event.status == EventStatus.COMPLETED + assert event.message == "done" + assert event.details == {"ok": True} + assert event.completed_at is not None + + +@pytest.mark.django_db +def test_event_complete_can_mark_failure(processing): + event = event_start( + processing=processing, + action=ProcessingAction.XML_VALIDATION, + task_id="task-999", + ) + + event_complete(event, message="broken", details={"error": "boom"}, status=EventStatus.FAILED) + + event.refresh_from_db() + assert event.status == EventStatus.FAILED + assert event.message == "broken" + assert event.details == {"error": "boom"} + + +@pytest.mark.django_db +def test_save_artifact_versions_current_artifact(processing, article): + first = save_artifact( + processing, + ArtifactType.XML, + "article.xml", + b"<article />", + article=article, + ) + second = save_artifact( + processing, + ArtifactType.XML, + "article.xml", + b"<article updated />", + article=article, + ) + + first.refresh_from_db() + second.refresh_from_db() + assert first.version == 1 + assert second.version == 2 + assert first.is_current is False + assert second.is_current is True + assert second.checksum != first.checksum + + +@pytest.mark.django_db +def test_create_structure_version_increments_version_and_marks_current(processing, article): + first = create_structure_version( + article, + processing, + InputType.XML, + front=[], + body=[], + back=[], + ) + second = create_structure_version( + article, + processing, + InputType.XML, + front=[{"type": "paragraph", "value": {"label": "<p>", "paragraph": "Updated"}}], + body=[], + back=[], + ) + + first.refresh_from_db() + second.refresh_from_db() + assert first.version == 1 + assert second.version == 2 + assert first.is_current is False + assert second.is_current is True + assert ArticleStructureVersion.objects.filter(article=article).count() == 2 diff --git a/manuscripts/tests/test_structure.py b/manuscripts/tests/test_structure.py new file mode 100644 index 0000000..0fbb0b0 --- /dev/null +++ b/manuscripts/tests/test_structure.py @@ -0,0 +1,212 @@ +import pytest + +from ai.utils.normalizers import stz_norm +from manuscripts.choices import InputType +from manuscripts.models.article import ArticleReference, ArticleStructureVersion, CitationOccurrence +from manuscripts.structure import create_structure_version, sync_references +from manuscripts.utils.helpers import checksum_bytes +from references.models import Reference, ReferenceStatus + + +def _ref_block(position, text, refid=None): + value = { + "label": "<p>", + "paragraph": text, + "authors": [], + } + if refid is not None: + value["refid"] = refid + return {"type": "ref_paragraph", "value": value} + + +def _paragraph_block(text): + return {"type": "paragraph", "value": {"label": "<p>", "paragraph": text}} + + +def _make_structure(article, processing, body=None, back=None): + return ArticleStructureVersion.objects.create( + article=article, + processing=processing, + version=1, + is_current=True, + source_kind=InputType.XML, + front=[], + body=body or [], + back=back or [], + creator=processing.creator, + ) + + +@pytest.mark.django_db +def test_sync_references_creates_linked_and_orphan_citations(processing, article): + back = [ + _ref_block(1, "Smith J. Linked reference. 2020.", refid="B1"), + {"type": "paragraph", "value": {"label": "<p>", "paragraph": "Not a reference block."}}, + ] + body = [ + _paragraph_block( + 'As shown in <xref ref-type="bibr" rid="B1"><bold>Smith, 2020</bold></xref>.' + ), + _paragraph_block( + 'Unknown cite <xref ref-type="bibr" rid="B99">Missing, 1999</xref>.' + ), + ] + structure = _make_structure(article, processing, body=body, back=back) + + sync_references(structure) + + references = list(structure.references.order_by("position")) + assert len(references) == 1 + assert references[0].ref_id == "B1" + assert references[0].position == 1 + assert references[0].mixed_citation == "Smith J. Linked reference. 2020." + + citations = list(structure.citations.order_by("created")) + assert len(citations) == 2 + + linked = citations[0] + assert linked.status == CitationOccurrence.Status.LINKED + assert linked.text == "Smith, 2020" + assert linked.location == {"section": "body", "block": 0} + assert list(linked.references.all()) == [references[0]] + + orphan = citations[1] + assert orphan.status == CitationOccurrence.Status.ORPHAN + assert orphan.text == "Missing, 1999" + assert orphan.location == {"section": "body", "block": 1} + assert list(orphan.references.all()) == [] + + +@pytest.mark.django_db +def test_sync_references_defaults_ref_id_from_position(processing, article): + back = [ + {"type": "paragraph", "value": {"label": "<p>", "paragraph": "Ignored block."}}, + _ref_block(2, "Jones A. Default ref id. 2019."), + ] + structure = _make_structure(article, processing, back=back) + + sync_references(structure) + + reference = structure.references.get() + assert reference.ref_id == "B2" + assert reference.position == 2 + + +@pytest.mark.django_db +def test_sync_references_creates_reference_and_schedules_get_reference( + processing, article, monkeypatch +): + delayed = [] + monkeypatch.setattr( + "manuscripts.structure.get_reference.delay", + lambda pk: delayed.append(pk), + ) + monkeypatch.setattr( + "manuscripts.structure.transaction.on_commit", + lambda callback: callback(), + ) + + back = [_ref_block(1, "New reference text for async enrichment.")] + structure = _make_structure(article, processing, back=back) + + sync_references(structure) + + global_reference = Reference.objects.get() + assert global_reference.status == ReferenceStatus.CREATING + assert delayed == [global_reference.pk] + assert structure.references.get().reference == global_reference + + +@pytest.mark.django_db +def test_sync_references_reuses_existing_reference_by_checksum(processing, article, user, monkeypatch): + text = "Reused reference text. 2018." + existing = Reference.objects.create( + mixed_citation=text, + status=ReferenceStatus.READY, + creator=user, + ) + delayed = [] + monkeypatch.setattr( + "manuscripts.structure.get_reference.delay", + lambda pk: delayed.append(pk), + ) + monkeypatch.setattr( + "manuscripts.structure.transaction.on_commit", + lambda callback: callback(), + ) + + back = [_ref_block(1, text, refid="B1")] + structure = _make_structure(article, processing, back=back) + + sync_references(structure) + + assert Reference.objects.count() == 1 + assert delayed == [] + article_reference = structure.references.get() + assert article_reference.reference == existing + expected_checksum = checksum_bytes(stz_norm(text).encode("utf-8")) + assert existing.checksum == expected_checksum + + +@pytest.mark.django_db +def test_sync_references_replaces_previous_references_and_citations(processing, article): + initial_back = [_ref_block(1, "First version.", refid="B1")] + structure = _make_structure(article, processing, back=initial_back) + sync_references(structure) + assert structure.references.count() == 1 + assert structure.citations.count() == 0 + + structure.back = [_ref_block(1, "Second version.", refid="B1")] + structure.save(update_fields=["back"]) + sync_references(structure) + + assert structure.references.count() == 1 + assert structure.references.get().mixed_citation == "Second version." + assert structure.citations.count() == 0 + assert ArticleReference.objects.filter(structure=structure).count() == 1 + assert CitationOccurrence.objects.filter(structure=structure).count() == 0 + + +@pytest.mark.django_db +def test_create_structure_version_increments_version_and_syncs_references(processing, article, monkeypatch): + monkeypatch.setattr( + "manuscripts.structure.get_reference.delay", + lambda _pk: None, + ) + monkeypatch.setattr( + "manuscripts.structure.transaction.on_commit", + lambda callback: callback(), + ) + + first = create_structure_version( + article, + processing, + InputType.XML, + front=[], + body=[], + back=[_ref_block(1, "Versioned reference.", refid="B1")], + base_xml="<article />", + warnings=["warn"], + xref_status={"B1": "linked"}, + ) + second = create_structure_version( + article, + processing, + InputType.DOCUMENT, + front=[_paragraph_block("Front matter")], + body=[], + back=[], + ) + + first.refresh_from_db() + second.refresh_from_db() + + assert first.version == 1 + assert second.version == 2 + assert first.is_current is False + assert second.is_current is True + assert first.base_xml == "<article />" + assert first.roundtrip_warnings == ["warn"] + assert first.xref_status == {"B1": "linked"} + assert first.references.count() == 1 + assert second.references.count() == 0 diff --git a/manuscripts/tests/test_tasks.py b/manuscripts/tests/test_tasks.py new file mode 100644 index 0000000..45ee085 --- /dev/null +++ b/manuscripts/tests/test_tasks.py @@ -0,0 +1,15 @@ +from unittest.mock import MagicMock + +import pytest + +from manuscripts.tasks import process_input + + +@pytest.mark.django_db +def test_process_input_task_delegates_to_processing(processing, monkeypatch): + delegate = MagicMock() + monkeypatch.setattr("manuscripts.tasks.processing.process_input", delegate) + + process_input.run(processing.pk, start_action="xml_validation") + + delegate.assert_called_once_with(process_input, processing.pk, "xml_validation") diff --git a/manuscripts/tests/test_utils.py b/manuscripts/tests/test_utils.py new file mode 100644 index 0000000..f19df13 --- /dev/null +++ b/manuscripts/tests/test_utils.py @@ -0,0 +1,61 @@ +import pytest + +from manuscripts.choices import InputType, ProcessingAction +from manuscripts.utils.helpers import checksum_bytes, json_safe +from manuscripts.utils.inspection import inspect_input, resolve_actions, suggested_actions + + +def test_checksum_bytes_returns_sha256_hexdigest(): + assert checksum_bytes(b"sample") == checksum_bytes(b"sample") + assert checksum_bytes(b"sample") != checksum_bytes(b"other") + + +def test_json_safe_serializes_sets_as_sorted_lists(): + assert json_safe({"tags": {"b", "a"}}) == {"tags": ["a", "b"]} + + +def test_suggested_actions_for_xml_input(): + assert suggested_actions(InputType.XML) == [ + ProcessingAction.XML_VALIDATION, + ProcessingAction.SPS_PACKAGE_GENERATION, + ProcessingAction.HTML_GENERATION, + ProcessingAction.PDF_GENERATION, + ] + + +def test_resolve_actions_adds_dependencies_for_document_pipeline(): + actions = resolve_actions( + [ProcessingAction.XML_GENERATION], + InputType.DOCUMENT, + ) + + assert actions == [ + ProcessingAction.CITATION_MARKUP, + ProcessingAction.XML_GENERATION, + ] + + +def test_inspect_input_detects_xml_file(tmp_path): + xml_path = tmp_path / "article.xml" + xml_path.write_text("<article></article>", encoding="utf-8") + + result = inspect_input(str(xml_path)) + + assert result["detected_type"] == InputType.XML + assert result["contents"] == [{"path": "article.xml", "kind": "xml"}] + + +def test_inspect_input_returns_unknown_for_unsupported_extension(tmp_path): + raw_path = tmp_path / "article.txt" + raw_path.write_text("hello", encoding="utf-8") + + result = inspect_input(str(raw_path)) + + assert result["detected_type"] == InputType.UNKNOWN + assert result["contents"] == [] + assert result["suggested_actions"] == [] + + +def test_resolve_actions_ignores_invalid_actions(): + actions = resolve_actions(["not-real-action"], InputType.XML) + assert actions == [] diff --git a/manuscripts/tests/test_views.py b/manuscripts/tests/test_views.py new file mode 100644 index 0000000..bbaade8 --- /dev/null +++ b/manuscripts/tests/test_views.py @@ -0,0 +1,101 @@ +from unittest.mock import MagicMock + +import pytest +from django.urls import reverse + +from manuscripts.artifacts import save_artifact +from manuscripts.choices import ArtifactType, InputType, ProcessingAction, ProcessStatus + + +@pytest.mark.django_db +def test_processing_review_post_starts_processing(staff_client, xml_processing, monkeypatch): + delay = MagicMock() + monkeypatch.setattr("manuscripts.views.editorial.process_input.delay", delay) + + response = staff_client.post( + reverse("manuscripts:processing_review", kwargs={"pk": xml_processing.pk}), + {"requested_actions": [ProcessingAction.XML_VALIDATION]}, + ) + + xml_processing.refresh_from_db() + assert response.status_code == 302 + assert xml_processing.status == ProcessStatus.PENDING + assert xml_processing.confirmed_type == InputType.XML + assert xml_processing.requested_actions == [ProcessingAction.XML_VALIDATION] + delay.assert_called_once_with(xml_processing.pk) + + +@pytest.mark.django_db +def test_processing_cancel_marks_processing_as_cancelled(staff_client, processing): + response = staff_client.post( + reverse("manuscripts:processing_cancel", kwargs={"pk": processing.pk}), + ) + + processing.refresh_from_db() + assert response.status_code == 302 + assert processing.status == ProcessStatus.CANCELLED + + +@pytest.mark.django_db +def test_processing_reprocess_increments_retry_and_dispatches_task(staff_client, processing, monkeypatch): + delay = MagicMock() + monkeypatch.setattr("manuscripts.views.editorial.process_input.delay", delay) + + response = staff_client.post( + reverse("manuscripts:processing_reprocess", kwargs={"pk": processing.pk}), + {"action": ProcessingAction.XML_VALIDATION}, + ) + + processing.refresh_from_db() + assert response.status_code == 302 + assert processing.retry_count == 1 + delay.assert_called_once_with(processing.pk, start_action=ProcessingAction.XML_VALIDATION) + + +@pytest.mark.django_db +def test_processing_reprocess_ignores_invalid_action(staff_client, processing, monkeypatch): + delay = MagicMock() + monkeypatch.setattr("manuscripts.views.editorial.process_input.delay", delay) + + staff_client.post( + reverse("manuscripts:processing_reprocess", kwargs={"pk": processing.pk}), + {"action": "invalid-action"}, + ) + + delay.assert_called_once_with(processing.pk, start_action=None) + + +@pytest.mark.django_db +def test_artifact_download_returns_file_content(staff_client, processing, article): + artifact = save_artifact( + processing, + ArtifactType.XML, + "article.xml", + b"<article />", + article=article, + ) + + response = staff_client.get( + reverse("manuscripts:artifact_download", kwargs={"pk": artifact.pk}), + ) + + assert response.status_code == 200 + assert b"".join(response.streaming_content) == b"<article />" + + +@pytest.mark.django_db +def test_processing_cancel_get_is_not_allowed(staff_client, processing): + response = staff_client.get( + reverse("manuscripts:processing_cancel", kwargs={"pk": processing.pk}), + ) + + assert response.status_code == 405 + + +@pytest.mark.django_db +def test_processing_reprocess_get_is_not_allowed(staff_client, processing): + response = staff_client.get( + reverse("manuscripts:processing_reprocess", kwargs={"pk": processing.pk}), + ) + + assert response.status_code == 405 diff --git a/manuscripts/tests/test_wagtail_hooks.py b/manuscripts/tests/test_wagtail_hooks.py new file mode 100644 index 0000000..8baa673 --- /dev/null +++ b/manuscripts/tests/test_wagtail_hooks.py @@ -0,0 +1,123 @@ +from types import SimpleNamespace +from unittest.mock import MagicMock + +import pytest +from django.urls import reverse + +from manuscripts.choices import ArtifactType, InputType, ProcessStatus +from manuscripts.wagtail_hooks import ( + article_listing_buttons, + processing_listing_buttons, + register_manuscripts_icons, + simplify_editorial_menu, +) + + +@pytest.mark.django_db +def test_register_manuscripts_icons_appends_custom_icons(): + icons = ["existing.svg"] + result = register_manuscripts_icons(icons) + assert result == icons + [ + "wagtailadmin/icons/package-zip.svg", + "wagtailadmin/icons/doc-docx.svg", + ] + + +@pytest.mark.parametrize( + "status,expected_label,expected_url_name", + [ + (ProcessStatus.AWAITING_REVIEW, "Review", "manuscripts:processing_review"), + (ProcessStatus.COMPLETED, "View trail", "wagtailsnippets_manuscripts_processing:inspect"), + ], +) +@pytest.mark.django_db +def test_processing_listing_buttons_for_processing_status( + processing, staff_user, status, expected_label, expected_url_name +): + processing.status = status + processing.save(update_fields=["status"]) + + buttons = list(processing_listing_buttons(processing, staff_user)) + assert len(buttons) == 1 + assert buttons[0].label == expected_label + assert buttons[0].url == reverse(expected_url_name, args=[processing.pk]) + + +@pytest.mark.django_db +def test_processing_listing_buttons_ignores_non_processing_snippets(article, staff_user): + assert list(processing_listing_buttons(article, staff_user)) == [] + + +@pytest.mark.django_db +def test_article_listing_buttons_structure_and_validation(article, processing, staff_user): + from manuscripts.artifacts import save_artifact + from manuscripts.structure import create_structure_version + + structure = create_structure_version(article, processing, InputType.DOCUMENT, [], [], []) + save_artifact( + processing, + ArtifactType.VALIDATION_REPORT, + "report.json", + b'[{"group": "test"}]', + article=article, + ) + + buttons = list(article_listing_buttons(article, staff_user)) + labels = [button.label for button in buttons] + assert "View Structure" in labels + assert "View Validation" in labels + assert buttons[0].attrs == {"target": "_blank"} + assert structure is not None + + +@pytest.mark.django_db +def test_article_listing_buttons_all_artifact_types(article, processing, staff_user): + from manuscripts.artifacts import save_artifact + + artifact_specs = [ + (ArtifactType.HTML, "Preview HTML", "artifact_preview"), + (ArtifactType.SPS_PACKAGE, "Download SPS Package", "artifact_download"), + (ArtifactType.MARKED_DOCUMENT, "Download Marked DOCX", "artifact_download"), + (ArtifactType.XML, "Download XML", "artifact_download"), + ] + for artifact_type, _label, _url_name in artifact_specs: + save_artifact( + processing, + artifact_type, + f"{artifact_type}.bin", + b"content", + article=article, + ) + + buttons = list(article_listing_buttons(article, staff_user)) + labels = [button.label for button in buttons] + assert labels == [ + "Preview HTML", + "Download SPS Package", + "Download Marked DOCX", + "Download XML", + ] + preview_button = next(button for button in buttons if button.label == "Preview HTML") + assert preview_button.attrs == {"target": "_blank"} + + +def test_article_listing_buttons_ignores_non_article_snippets(processing, staff_user): + assert list(article_listing_buttons(processing, staff_user)) == [] + + +def test_article_listing_buttons_without_structure_or_artifacts(article, staff_user): + assert list(article_listing_buttons(article, staff_user)) == [] + + +def test_simplify_editorial_menu_hides_snippets_and_scielo(): + request = MagicMock() + menu_items = [ + SimpleNamespace(name="snippets"), + SimpleNamespace(name="articles"), + SimpleNamespace(name="scielo"), + SimpleNamespace(name="images"), + ] + + simplify_editorial_menu(request, menu_items) + + assert [item.name for item in menu_items] == ["articles", "images"] diff --git a/manuscripts/tests/test_wagtail_views.py b/manuscripts/tests/test_wagtail_views.py new file mode 100644 index 0000000..0b9dba6 --- /dev/null +++ b/manuscripts/tests/test_wagtail_views.py @@ -0,0 +1,231 @@ +from unittest.mock import MagicMock, patch + +import pytest +from django.test import RequestFactory +from django.urls import reverse + +from manuscripts.choices import ArtifactType, InputType +from manuscripts.forms import ( + DOCXUploadForm, + ProcessingUploadForm, + SPSPackageUploadForm, + XMLUploadForm, +) +from manuscripts.models.processing import ProcessingEvent +from manuscripts.views.wagtail import ( + ArticleInputCreateView, + ArticleInspectView, + OwnedCreateView, + ProcessingCreateView, + ProcessingInspectView, + SPSPackageImportCreateView, + XMLImportCreateView, +) + + +@pytest.fixture +def request_factory(): + return RequestFactory() + + +@pytest.mark.django_db +def test_processing_create_view_save_instance_calls_inspect_processing( + request_factory, staff_user, monkeypatch +): + inspected = [] + monkeypatch.setattr( + "manuscripts.views.wagtail.inspect_processing", + lambda processing: inspected.append(processing), + ) + + processing = MagicMock() + view = ProcessingCreateView() + view.request = request_factory.get("/") + view.request.user = staff_user + view.form = MagicMock() + view.form.instance = MagicMock() + + with patch.object(ProcessingCreateView.__bases__[0], "save_instance", return_value=processing): + result = view.save_instance() + + assert result is processing + assert view.form.instance.creator is staff_user + assert inspected == [processing] + + +@pytest.mark.parametrize( + "view_class,expected_form", + [ + (ProcessingCreateView, ProcessingUploadForm), + (XMLImportCreateView, XMLUploadForm), + (ArticleInputCreateView, DOCXUploadForm), + (SPSPackageImportCreateView, SPSPackageUploadForm), + ], +) +def test_processing_create_view_subclasses_form_class(view_class, expected_form): + assert view_class().get_form_class() is expected_form + + +@pytest.mark.django_db +def test_owned_create_view_sets_creator(request_factory, staff_user): + view = OwnedCreateView() + view.request = request_factory.get("/") + view.request.user = staff_user + view.form = MagicMock() + view.form.instance = MagicMock() + saved = MagicMock() + + with patch.object(OwnedCreateView.__bases__[0], "save_instance", return_value=saved): + result = view.save_instance() + + assert result is saved + assert view.form.instance.creator is staff_user + + +@pytest.mark.django_db +def test_processing_create_view_success_url(processing): + view = ProcessingCreateView() + view.object = processing + assert view.get_success_url() == reverse("manuscripts:processing_review", args=[processing.pk]) + + +@pytest.mark.django_db +def test_processing_inspect_view_get_context_data(processing, article, user): + from manuscripts.artifacts import save_artifact + + processing.articles.add(article) + ProcessingEvent.objects.create( + processing=processing, + article=article, + creator=user, + message="Trail event", + ) + artifact = save_artifact( + processing, + ArtifactType.XML, + "nested/path/article.xml", + b"<article />", + article=article, + ) + + view = ProcessingInspectView() + view.object = processing + context = view.get_context_data() + + assert context["events"].count() == 1 + xml_row = next(row for row in context["artifact_rows"] if row["artifact"] == artifact) + assert xml_row["filename"].startswith("article") + assert xml_row["filename"].endswith(".xml") + assert article in context["articles"] + assert context["action_choices"] + + +@pytest.mark.django_db +def test_article_inspect_view_get_artifact_rows_uses_current_xml_structure( + processing, article, user +): + from manuscripts.artifacts import save_artifact + from manuscripts.structure import create_structure_version + + structure = create_structure_version( + article, + processing, + InputType.XML, + [], + [], + [], + ) + xml_artifact = save_artifact( + processing, + ArtifactType.XML, + "article.xml", + b"<article />", + article=article, + structure=structure, + ) + html_artifact = save_artifact( + processing, + ArtifactType.HTML, + "article.html", + b"<html />", + article=article, + ) + + view = ArticleInspectView() + view.object = article + rows = view.get_artifact_rows([xml_artifact, html_artifact]) + + xml_row = next(row for row in rows if row["artifact"].artifact_type == ArtifactType.XML) + html_row = next(row for row in rows if row["artifact"].artifact_type == ArtifactType.HTML) + assert xml_row["structure"] is structure + assert html_row["structure"] == structure + assert html_row["order"] == 20 + + +@pytest.mark.django_db +def test_article_inspect_view_get_context_data(processing, article, user): + from manuscripts.artifacts import save_artifact + from manuscripts.models.processing import ProcessingEvent + from manuscripts.structure import create_structure_version + + processing.articles.add(article) + structure = create_structure_version( + article, + processing, + InputType.XML, + [], + [{"type": "paragraph", "value": {"label": "<p>", "paragraph": "Body"}}], + [{"type": "ref_paragraph", "value": {"label": "<p>", "paragraph": "Ref", "refid": "B1"}}], + ) + stale_xml = save_artifact( + processing, + ArtifactType.XML, + "old.xml", + b"<old />", + article=article, + structure=structure, + ) + current_xml = save_artifact( + processing, + ArtifactType.XML, + "article.xml", + b"<article />", + article=article, + structure=structure, + ) + save_artifact( + processing, + ArtifactType.HTML, + "article.html", + b"<html />", + article=article, + ) + save_artifact( + processing, + ArtifactType.VALIDATION_REPORT, + "report.json", + b"[]", + article=article, + ) + ProcessingEvent.objects.create( + processing=processing, + article=article, + creator=user, + details={"frontmatter_ai": {"title": "AI title"}}, + ) + + view = ArticleInspectView() + view.object = article + context = view.get_context_data() + + assert context["processings"] == [processing] + assert context["processing_rows"][0]["frontmatter_ai"] == {"title": "AI title"} + assert len(context["current_artifact_rows"]) >= 2 + assert any(row["artifact"].pk == stale_xml.pk for row in context["historical_artifact_rows"]) + assert context["structure"] == structure + assert context["references"].count() == 1 + assert context["citations"].count() >= 0 + assert context["latest_processing"] == processing + assert context["current_xml_artifact"].pk == current_xml.pk + assert context["current_html_artifact"] is not None + assert context["current_validation_report_artifact"] is not None diff --git a/manuscripts/tests/test_xml_utils.py b/manuscripts/tests/test_xml_utils.py new file mode 100644 index 0000000..c2fe109 --- /dev/null +++ b/manuscripts/tests/test_xml_utils.py @@ -0,0 +1,512 @@ +from datetime import date +from pathlib import Path + +import pytest +from lxml import etree + +from journals.models import Journal +from manuscripts.choices import InputType +from manuscripts.models.article import ArticleStructureVersion +from manuscripts.utils.xml_utils import ( + _article_adapter, + _get_inner_xml, + _plain_text, + extract_article_metadata, + generate_structure_xml, + parse_xml_structure, +) + +SOUZA_XML = Path(__file__).resolve().parents[2] / "fixtures" / "xml" / "souza_2021.xml" + +COMPREHENSIVE_XML = b"""<?xml version="1.0" encoding="utf-8"?> +<article xml:lang="pt" xmlns:xml="http://www.w3.org/XML/1998/namespace"> + <front> + <article-meta> + <title-group> + <article-title>Main Title</article-title> + <trans-title-group xml:lang="en"> + <trans-title>English Title</trans-title> + </trans-title-group> + <trans-title-group xml:lang="es"> + <trans-title></trans-title> + </trans-title-group> + </title-group> + <contrib-group> + <contrib contrib-type="author"> + <contrib-id contrib-id-type="orcid">0000-0001</contrib-id> + <name> + <given-names>John</given-names> + <surname>Doe</surname> + </name> + <xref ref-type="aff" rid="aff1">1</xref> + </contrib> + <contrib contrib-type="author"> + <name> + <given-names>Jane</given-names> + <surname>Smith</surname> + </name> + </contrib> + <contrib contrib-type="editor"> + <name><surname>Editor</surname></name> + </contrib> + </contrib-group> + <aff id="aff1"> + <label>1</label> + <institution content-type="orgname">University</institution> + <institution content-type="orgdiv1">Dept</institution> + <institution content-type="orgdiv2">Lab</institution> + <addr-line> + <city>Campinas</city> + <state>SP</state> + <country country="BR">Brazil</country> + </addr-line> + </aff> + <abstract> + <title>Resumo +

    Abstract text here.

    +
    + +

    Trans abstract.

    +
    + + Palavras-chave + alpha + beta + + + + only + + + + + + + Section One +

    Body paragraph.

    +
    + + + + First ref + Second ref + + +
    +""" + +METADATA_XML = b""" +
    + + + 10.1234/TEST + PUB-123 + + Metadata Title + + e12345 + 10 + 20 + + 99 + 13 + 2024 + + + + + + + + + + +
    +""" + + +def _make_structure(article, processing, **kwargs): + return ArticleStructureVersion.objects.create( + article=article, + processing=processing, + version=1, + is_current=True, + source_kind=InputType.XML, + front=kwargs.get("front", []), + body=kwargs.get("body", []), + back=kwargs.get("back", []), + base_xml=kwargs.get("base_xml", ""), + creator=processing.creator, + ) + + +def test_plain_text_collapses_whitespace(): + node = etree.fromstring(" Hello <italic>world</italic> ") + assert _plain_text(node) == "Hello world" + + +def test_get_inner_xml_strips_p_wrapper(): + node = etree.fromstring("

    Inner text.

    ") + assert _get_inner_xml(node) == "Inner text." + + +def test_get_inner_xml_returns_raw_xml_without_p_wrapper(): + node = etree.fromstring("standalone") + assert _get_inner_xml(node) == "standalone" + + +def test_parse_xml_structure_extracts_all_sections_and_warnings(): + front, body, back, warnings = parse_xml_structure(COMPREHENSIVE_XML) + + assert front[0] == { + "type": "paragraph_with_language", + "value": {"label": "", "language": "pt", "paragraph": "Main Title"}, + } + assert front[1]["value"]["label"] == "" + assert front[1]["value"]["paragraph"] == "English Title" + + authors = [item for item in front if item["type"] == "author_paragraph"] + assert len(authors) == 2 + assert authors[0]["value"] == { + "label": "", + "paragraph": "John Doe", + "surname": "Doe", + "given_names": "John", + "orcid": "0000-0001", + "affid": "1", + "char": "1", + } + assert authors[1]["value"]["char"] == "" + + aff = next(item for item in front if item["type"] == "aff_paragraph") + assert aff["value"]["affid"] == "1" + assert aff["value"]["orgname"] == "University" + assert aff["value"]["city"] == "Campinas" + assert aff["value"]["code_country"] == "BR" + + abstract_titles = [ + item["value"]["paragraph"] + for item in front + if item["type"] == "paragraph" and item["value"].get("label") == "" + ] + assert abstract_titles == ["Resumo", "Abstract"] + + abstract_blocks = [item for item in front if item["value"].get("label") == ""] + assert abstract_blocks[0]["value"]["paragraph"] == "Abstract text here." + assert abstract_blocks[1]["value"]["language"] == "en" + assert abstract_blocks[1]["value"]["paragraph"] == "Trans abstract." + + kwd_titles = [item for item in front if item["value"].get("label") == ""] + assert kwd_titles[0]["value"]["paragraph"] == "Palavras-chave" + assert kwd_titles[1]["value"]["paragraph"] == "Keywords" + + assert body == [ + {"type": "paragraph", "value": {"label": "", "paragraph": "Section One"}}, + {"type": "paragraph", "value": {"label": "

    ", "paragraph": "Body paragraph."}}, + ] + + assert len(back) == 2 + assert back[0]["value"]["refid"] == "R1" + assert back[0]["value"]["paragraph"] == "First ref" + assert back[1]["value"]["refid"] == "B2" + assert back[1]["value"]["paragraph"] == "Second ref" + + assert len(warnings) == 1 + assert warnings[0]["code"] == "UNMODELED_XML_NODES" + assert "permissions" in warnings[0]["nodes"] + assert warnings[0]["message"] == "Nós preservados somente no XML-base." + + +def test_parse_xml_structure_handles_minimal_xml_without_warnings(): + xml = b"

    " + front, body, back, warnings = parse_xml_structure(xml) + assert front == [] + assert body == [] + assert back == [] + assert warnings == [] + + +def test_parse_xml_structure_with_souza_fixture(): + content = SOUZA_XML.read_bytes() + front, body, back, warnings = parse_xml_structure(content) + + assert any(item["value"].get("label") == "" for item in front) + assert any(item["type"] == "author_paragraph" for item in front) + assert any(item["type"] == "aff_paragraph" for item in front) + assert body + assert back + assert warnings + + +def test_extract_article_metadata_all_fields_and_normalization(): + metadata = extract_article_metadata(METADATA_XML) + + assert metadata["title"] == "Metadata Title" + assert metadata["doi"] == "10.1234/test" + assert metadata["pid"] == "PUB-123" + assert "figures/fig1.png" in metadata["assets"] + assert all(not asset.startswith("#") for asset in metadata["assets"]) + assert metadata["language"] == "pt" + assert metadata["license"] == "https://creativecommons.org/licenses/by/4.0/" + assert metadata["elocatid"] == "e12345" + assert metadata["fpage"] == "10" + assert metadata["lpage"] == "20" + assert metadata["seq"] == "1" + assert metadata["artdate"] == "2024-01-01" + + +def test_extract_article_metadata_falls_back_to_other_pid_and_defaults(): + xml = b"""
    + + OTHER-99 + 32023 + +
    """ + metadata = extract_article_metadata(xml) + + assert metadata["title"] == "" + assert metadata["pid"] == "OTHER-99" + assert metadata["language"] == "en" + assert metadata["license"] == "" + assert metadata["assets"] == [] + assert metadata["artdate"] == "2023-03-01" + + +def test_extract_article_metadata_skips_artdate_without_year(): + xml = b"""
    + 510 +
    """ + assert extract_article_metadata(xml)["artdate"] is None + + +def test_extract_article_metadata_normalizes_non_digit_month_and_day(): + xml = b"""
    + abxy2022 +
    """ + assert extract_article_metadata(xml)["artdate"] == "2022-01-01" + + +def test_article_adapter_without_journal(article): + article.language = "" + article.license = None + article.elocatid = "" + article.fpage = "" + article.lpage = "" + article.seq = "" + article.artdate = None + article.ahpdate = None + + adapted = _article_adapter(article) + + assert adapted.title == article.title + assert adapted.content == [] + assert adapted.acronym == "" + assert adapted.title_nlm == "" + assert adapted.journal_title == "" + assert adapted.short_title == "" + assert adapted.pissn == "" + assert adapted.eissn == "" + assert adapted.pubname == "" + assert adapted.issue is None + assert adapted.language == "en" + assert adapted.license == "" + assert adapted.dateiso == "" + + +@pytest.mark.django_db +def test_article_adapter_with_journal(article, db): + journal = Journal.objects.create( + title="Journal Title", + short_title="J Short", + title_nlm="J NLM", + acronym="JT", + pissn="1111-1111", + eissn="2222-2222", + publisher_name="Publisher", + ) + article.journal = journal + article.language = "pt" + article.license = "https://license.example" + article.elocatid = "e1" + article.fpage = "1" + article.lpage = "9" + article.seq = "2" + article.artdate = date(2024, 6, 15) + + adapted = _article_adapter(article) + + assert adapted.acronym == "JT" + assert adapted.title_nlm == "J NLM" + assert adapted.journal_title == "Journal Title" + assert adapted.short_title == "J Short" + assert adapted.pissn == "1111-1111" + assert adapted.eissn == "2222-2222" + assert adapted.pubname == "Publisher" + assert adapted.language == "pt" + assert adapted.license == "https://license.example" + assert adapted.elocatid == "e1" + assert adapted.fpage == "1" + assert adapted.lpage == "9" + assert adapted.seq == "2" + assert adapted.dateiso == "2024-06-15" + + +@pytest.mark.django_db +def test_generate_structure_xml_without_base_xml(processing, article, monkeypatch): + structure = _make_structure( + article, + processing, + body=[{"type": "paragraph", "value": {"label": "

    ", "paragraph": "Body"}}], + ) + monkeypatch.setattr( + "manuscripts.utils.xml_utils.get_xml", + lambda article_docx, front, body, back: ('

    ', body), + ) + + result = generate_structure_xml(structure) + + assert result == b'
    ' + + +@pytest.mark.django_db +def test_generate_structure_xml_saves_normalized_body(processing, article, monkeypatch): + initial_body = [{"type": "paragraph", "value": {"label": "

    ", "paragraph": "Before"}}] + normalized_body = [{"type": "paragraph", "value": {"label": "

    ", "paragraph": "After"}}] + structure = _make_structure(article, processing, body=initial_body) + monkeypatch.setattr( + "manuscripts.utils.xml_utils.get_xml", + lambda *_args: ('

    ', normalized_body), + ) + + generate_structure_xml(structure) + + structure.refresh_from_db() + assert len(structure.body) == 1 + assert structure.body[0].block_type == "paragraph" + assert structure.body[0].value["paragraph"] == "After" + + +@pytest.mark.django_db +def test_generate_structure_xml_merges_base_xml_paths(processing, article, monkeypatch): + base_xml = """ +
    + + + Old title + + + + + + + + + Old body + +
    """ + structure = _make_structure(article, processing, base_xml=base_xml) + + generated_xml = """ +
    + + + New title + New + +

    New abstract

    +

    Trans

    + term +
    +
    + New body

    Paragraph

    + New ref +
    """ + + monkeypatch.setattr( + "manuscripts.utils.xml_utils.get_xml", + lambda *_args: (generated_xml, []), + ) + + result = generate_structure_xml(structure) + merged = etree.fromstring(result) + + assert merged.xpath("string(.//article-title)") == "New title" + assert merged.xpath("string(.//contrib/name/surname)") == "New" + assert merged.xpath("string(.//aff/@id)") == "aff2" + assert merged.xpath("string(.//abstract/p)") == "New abstract" + assert merged.xpath("string(.//trans-abstract/p)") == "Trans" + assert merged.xpath("string(.//kwd-group/kwd)") == "term" + assert merged.xpath("string(.//body/sec/title)") == "New body" + assert merged.xpath("string(.//ref/@id)") == "new" + assert merged.xpath(".//permissions") == [] + + +@pytest.mark.django_db +def test_generate_structure_xml_appends_when_target_missing(processing, article, monkeypatch): + base_xml = """ +
    + + + +
    """ + structure = _make_structure(article, processing, base_xml=base_xml) + + generated_xml = """ +
    + + + Appended title + keyword + + +

    Body

    + +
    """ + + monkeypatch.setattr( + "manuscripts.utils.xml_utils.get_xml", + lambda *_args: (generated_xml, []), + ) + + result = generate_structure_xml(structure) + merged = etree.fromstring(result) + article_meta = merged.xpath(".//article-meta")[0] + + assert merged.xpath("string(.//kwd-group/kwd)") == "keyword" + assert merged.xpath("string(.//body/sec/p)") == "Body" + assert merged.xpath("string(.//ref/@id)") == "B1" + assert article_meta.xpath("./kwd-group") != [] + + +@pytest.mark.django_db +def test_generate_structure_xml_removes_targets_when_sources_empty(processing, article, monkeypatch): + base_xml = """ +
    + + + + + +

    Keep in base only

    + +
    """ + structure = _make_structure(article, processing, base_xml=base_xml) + + generated_xml = """ +
    + +
    """ + + monkeypatch.setattr( + "manuscripts.utils.xml_utils.get_xml", + lambda *_args: (generated_xml, []), + ) + + result = generate_structure_xml(structure) + merged = etree.fromstring(result) + + assert merged.xpath(".//permissions") == [] + assert merged.xpath(".//body") == [] + assert merged.xpath(".//ref-list") == [] diff --git a/manuscripts/urls.py b/manuscripts/urls.py new file mode 100644 index 0000000..7f60b8d --- /dev/null +++ b/manuscripts/urls.py @@ -0,0 +1,22 @@ +from django.urls import path + +from .views import editorial as editorial_views + +app_name = "manuscripts" + +urlpatterns = [ + path("processings//review/", editorial_views.processing_review, name="processing_review"), + path("processings//cancel/", editorial_views.processing_cancel, name="processing_cancel"), + path("processings//reprocess/", editorial_views.processing_reprocess, name="processing_reprocess"), + path("processings//cleanup-artifacts/", editorial_views.processing_cleanup_artifacts, name="processing_cleanup_artifacts"), + path("artifacts//download/", editorial_views.artifact_download, name="artifact_download"), + path("artifacts//preview/", editorial_views.artifact_preview, name="artifact_preview"), + path("articles//validation/", editorial_views.article_validation_view, name="article_validation"), + path("articles//structure/", editorial_views.article_structure_edit, name="article_structure_edit"), + path("articles//structure/revert//", editorial_views.article_structure_revert, name="article_structure_revert"), + path("articles//references/", editorial_views.article_references_edit, name="article_references_edit"), + path("articles//reprocess/", editorial_views.article_reprocess, name="article_reprocess"), + path("articles//cleanup-artifacts/", editorial_views.article_cleanup_artifacts, name="article_cleanup_artifacts"), + path("citations//update/", editorial_views.citation_update, name="citation_update"), + path("article-references//select/", editorial_views.article_reference_select, name="article_reference_select"), +] diff --git a/manuscripts/utils/docx_utils.py b/manuscripts/utils/docx_utils.py new file mode 100644 index 0000000..af9a893 --- /dev/null +++ b/manuscripts/utils/docx_utils.py @@ -0,0 +1,102 @@ +from docx_parser.parser import DocxParser +from labeling.segmentation import ( + _looks_like_frontmatter, + create_labeled_object, + create_special_content_object, +) +from sps.xref import build_text_xref_replacer, read_marks, validate_marks + +from manuscripts.utils.helpers import _block, _raw_reference + + +def _xref_map(document): + result = {} + for reference in read_marks(document): + rid = reference["rid"] + for citation in reference["citations"]: + if citation: + result[citation] = rid + return result + + +def _apply_xrefs(body, document): + replacer = build_text_xref_replacer(document) + xref_map = _xref_map(document) + for item in body: + value = item.get("value", {}) + if value.get("label") != "

    ": + continue + text = value.get("paragraph") or "" + for citation, rid in sorted(xref_map.items(), key=lambda pair: -len(pair[0])): + if citation in text and f">{citation}" not in text: + text = text.replace( + citation, f'{citation}' + ) + value["paragraph"] = replacer(text) + + +def extract_docx_structure(document, source_path): + sections, content = DocxParser().extract_content(document, source_path, merge_front=False) + front, body, back = [], [], [] + references = [] + state = { + "label": None, + "label_next": None, + "label_next_reset": None, + "reset": False, + "repeat": None, + "body_trans": False, + "body": False, + "back": False, + "references": False, + } + counts = {"numref": 0, "numtab": 0, "numfig": 0, "numeq": 0} + + for item in content: + item_type = item.get("type") + text = item.get("text") or "" + if item_type == "first_block": + if text.strip(): + front.append(_block("", text, "paragraph_with_language")) + continue + if item_type in {"image", "table", "list", "compound"}: + obj, counts = create_special_content_object(item, body, counts) + if obj: + image = obj.get("value", {}).get("image") + if hasattr(image, "pk"): + obj["value"]["image"] = image.pk + table_text = str( + obj.get("value", {}).get("title") or "" + ) + " " + str( + obj.get("value", {}).get("content") or "" + ) + if _looks_like_frontmatter(table_text): + front.append(obj) + else: + body.append(obj) + continue + if not text.strip(): + continue + + obj, _result, state = create_labeled_object(0, item, state, sections) + if not obj: + continue + label = obj.get("value", {}).get("label") + if state["back"] and label == "

    ": + references.append(text) + elif state["back"]: + back.append(obj) + elif state["body"]: + body.append(obj) + else: + front.append(obj) + + if not body: + body = [ + _block("

    ", item["text"]) + for item in content + if item.get("text") and item.get("type") not in {"first_block"} + ] + back.extend(_raw_reference(index, text) for index, text in enumerate(references, 1)) + _apply_xrefs(body, document) + return front, body, back, validate_marks(document) diff --git a/manuscripts/utils/frontmatter.py b/manuscripts/utils/frontmatter.py new file mode 100644 index 0000000..4147bd5 --- /dev/null +++ b/manuscripts/utils/frontmatter.py @@ -0,0 +1,187 @@ +import re + +from ai.utils.blocks import ( + block_parts, + block_text, + make_block, + make_lang_block, +) +from ai.utils.normalizers import stz_language, stz_norm + + +_TAG_RE = re.compile(r"<[^>]+>") + + +def enrich_frontmatter(original_front, payload, article): + blocks = [] + + if payload.get("doi"): + blocks.append(make_block("", payload["doi"])) + + titles = payload.get("titles") or [] + for index, item in enumerate(titles): + label = "" if index == 0 else "" + blocks.append(make_lang_block(label, item["text"], item.get("language") or article.language or "en")) + + for author in payload.get("authors") or []: + affids = ",".join(author.get("affiliations") or []) + blocks.append({ + "type": "author_paragraph", + "value": { + "label": "", + "paragraph": author["display"], + "surname": author.get("surname", ""), + "given_names": author.get("given_names", ""), + "orcid": author.get("orcid", ""), + "affid": affids, + "char": author.get("symbol", ""), + }, + }) + + for affiliation in payload.get("affiliations") or []: + blocks.append({ + "type": "aff_paragraph", + "value": { + "label": "", + "paragraph": affiliation["text"], + "affid": affiliation["id"], + "text_aff": affiliation["text"], + "char": affiliation.get("symbol", ""), + "orgname": affiliation.get("orgname", ""), + "orgdiv2": affiliation.get("orgdiv2", ""), + "orgdiv1": affiliation.get("orgdiv1", ""), + "zipcode": "", + "city": affiliation.get("city", ""), + "state": affiliation.get("state", ""), + "country": affiliation.get("country", ""), + "code_country": affiliation.get("country_code", ""), + "original": affiliation["text"], + }, + }) + + for item in payload.get("dates") or []: + date_text = item.get("date") or item.get("raw") + if item.get("type") == "received": + blocks.append(make_block("", date_text)) + elif item.get("type") == "accepted": + blocks.append(make_block("", date_text)) + + original_abstract_langs = set() + current_lang = None + for item in original_front or []: + _, value = block_parts(item) + label = value.get("label", "") + if label == "": + title_text = str(value.get("paragraph") or "").lower().strip() + if "resumen" in title_text: + current_lang = "es" + elif "resumo" in title_text: + current_lang = "pt" + elif "abstract" in title_text: + current_lang = "en" + elif label == "": + if value.get("language"): + original_abstract_langs.add(value.get("language")) + elif current_lang: + original_abstract_langs.add(current_lang) + + for item in payload.get("abstracts") or []: + lang = stz_language(item.get("language"), article.language) + if lang in original_abstract_langs: + continue + title = item.get("title") or {"pt": "Resumo", "en": "Abstract", "es": "Resumen"}.get(lang, "Abstract") + blocks.append(make_lang_block("", title, lang)) + blocks.append(make_lang_block("", item["text"], lang)) + + for item in payload.get("keywords") or []: + if item.get("title"): + blocks.append(make_lang_block("", item["title"], item.get("language") or article.language or "en")) + blocks.append(make_lang_block("", "; ".join(item["terms"]), item.get("language") or article.language or "en")) + + return blocks + _collect_remaining_blocks(original_front, payload) + + +def _collect_remaining_blocks(front, payload): + seen_texts = _collect_payload_texts(payload) + kept = [] + for item in front or []: + block_type, value = block_parts(item) + label = value.get("label", "") + text = block_text(value) + if _is_label_replaced(label, payload): + continue + if label == "" and payload.get("titles"): + continue + if label == "<author-notes>" and (payload.get("authors") or payload.get("affiliations")): + continue + if label == "<p>" and _is_text_covered(text, seen_texts): + continue + kept.append({"type": block_type, "value": value}) + return kept + + +def _is_label_replaced(label, payload): + if label == "<article-id>": + return bool(payload.get("doi")) + if label in {"<article-title>", "<trans-title>"}: + return bool(payload.get("titles")) + if label == "<contrib>": + return bool(payload.get("authors")) + if label == "<aff>": + return bool(payload.get("affiliations")) + date_types = {item.get("type") for item in payload.get("dates") or []} + if label == "<date-received>": + return "received" in date_types + if label == "<date-accepted>": + return "accepted" in date_types + if label == "<history>": + return bool(date_types) + if label in {"<abstract-title>", "<abstract>", "<trans-abstract>"}: + return False + if label in {"<kwd-title>", "<kwd-group>"}: + return bool(payload.get("keywords")) + return False + + +def _collect_payload_texts(payload): + texts = [] + if payload.get("doi"): + texts.append(payload["doi"]) + for item in payload.get("titles") or []: + texts.append(item.get("text", "")) + for item in payload.get("abstracts") or []: + texts.extend([item.get("title", ""), item.get("text", "")]) + for item in payload.get("keywords") or []: + texts.append(item.get("title", "")) + texts.append("; ".join(item.get("terms") or [])) + texts.append(", ".join(item.get("terms") or [])) + for item in payload.get("dates") or []: + texts.extend([item.get("date", ""), item.get("raw", "")]) + return {stz_norm(text) for text in texts if text} + + +def _is_text_covered(text, seen): + clean = stz_norm(text) + if not clean: + return True + if clean in seen: + return True + if any(clean and (clean in item or item in clean) and min(len(clean), len(item)) > 80 for item in seen): + return True + for item in seen: + if item and len(item) > 60 and item in clean: + return True + clean_no_tags = _TAG_RE.sub(" ", clean).strip() + clean_no_tags = re.sub(r"\s+", " ", clean_no_tags) + if clean_no_tags in seen: + return True + for item in seen: + if item and len(item) > 60 and item in clean_no_tags: + return True + return bool( + re.search(r"\b(resumen|resumo|abstract|palabras?\s+clave|palavras[-\s]chave|keywords?)\b", clean) + or re.search(r"\b(received|recibido|recebido|accepted|aceptado|aceito|published|publicado)\b", clean) + or re.search(r"\bdoi\b.*\b10\.\d{4,}|10\.\d{4,}/", clean, re.I) + or re.search(r"\b(orcid\.org/[0-9\-X]+|[0-9]{4}-[0-9]{4}-[0-9]{4}-[0-9]{3}[X\d])\b", clean) + or re.search(r"@\S+\.\S+", clean) + ) diff --git a/manuscripts/utils/helpers.py b/manuscripts/utils/helpers.py new file mode 100644 index 0000000..ec1ea71 --- /dev/null +++ b/manuscripts/utils/helpers.py @@ -0,0 +1,55 @@ +import hashlib +import json +import re +from pathlib import PurePosixPath + +XREF_RE = re.compile( + r'<xref\s+[^>]*ref-type=["\']bibr["\'][^>]*rid=["\']([^"\']+)["\'][^>]*>(.*?)</xref>', + re.I | re.S, +) + + +def checksum_bytes(content): + return hashlib.sha256(content).hexdigest() + + +def json_safe(value): + return json.loads( + json.dumps( + value, + ensure_ascii=False, + default=lambda item: sorted(item) if isinstance(item, set) else str(item), + ) + ) + + +def to_dict_list(stream_value): + if not stream_value: + return [] + if hasattr(stream_value, "stream_block"): + return stream_value.stream_block.get_prep_value(stream_value) + return stream_value + + +def _block(label, paragraph, block_type="paragraph"): + return {"type": block_type, "value": {"label": label, "paragraph": paragraph}} + + +def _raw_reference(position, text): + return { + "type": "ref_paragraph", + "value": { + "label": "<p>", + "refid": f"B{position}", + "paragraph": text, + "authors": [], + }, + } + + +def safe_archive_members(archive): + for info in archive.infolist(): + path = PurePosixPath(info.filename) + if info.is_dir() or path.is_absolute() or ".." in path.parts: + continue + yield info diff --git a/manuscripts/utils/ingestion.py b/manuscripts/utils/ingestion.py new file mode 100644 index 0000000..ca7b2af --- /dev/null +++ b/manuscripts/utils/ingestion.py @@ -0,0 +1,69 @@ +import os +import zipfile +from pathlib import PurePosixPath + +from manuscripts.artifacts import resolve_article_assets, save_artifact +from manuscripts.choices import ArtifactType, InputType +from manuscripts.controller import get_or_create_article +from manuscripts.structure import create_structure_version +from manuscripts.utils.xml_utils import parse_xml_structure +from manuscripts.utils.helpers import safe_archive_members + + +def ingest_xml(processing, name, content, original_path=""): + article, metadata = get_or_create_article(processing, content) + artifact = save_artifact( + processing, ArtifactType.XML, name, content, article, {"referenced_assets": metadata["assets"]}, original_path + ) + front, body, back, warnings = parse_xml_structure(content) + structure = create_structure_version( + article, + processing, + processing.confirmed_type or InputType.XML, + front, + body, + back, + base_xml=content.decode("utf-8"), + warnings=warnings, + ) + artifact.structure = structure + artifact.save(update_fields=["structure", "updated"]) + return article, artifact + + +def ingest_zip(processing): + xml_artifacts = [] + source_document = None + with zipfile.ZipFile(processing.input_file.path) as archive: + members = list(safe_archive_members(archive)) + documents = [item for item in members if PurePosixPath(item.filename).suffix.lower() == ".docx"] + if processing.confirmed_type == InputType.SOURCE_PACKAGE and len(documents) != 1: + raise ValueError("Pacote-fonte deve conter exatamente um documento suportado.") + for info in members: + content = archive.read(info) + suffix = PurePosixPath(info.filename).suffix.lower() + if suffix == ".xml": + xml_artifacts.append(ingest_xml(processing, info.filename, content, info.filename)) + elif suffix == ".docx": + source_document = save_artifact( + processing, ArtifactType.SOURCE_DOCUMENT, info.filename, content, original_path=info.filename + ) + else: + save_artifact(processing, ArtifactType.ASSET, info.filename, content, original_path=info.filename) + for article, xml_artifact in xml_artifacts: + resolve_article_assets( + processing, article, xml_artifact.metadata.get("referenced_assets", []) + ) + return source_document, xml_artifacts + + +def ingest_document(processing): + if processing.confirmed_type == InputType.SOURCE_PACKAGE: + source, _xmls = ingest_zip(processing) + if not source: + raise ValueError("Documento não encontrado no pacote-fonte.") + return source + with processing.input_file.open("rb") as source: + return save_artifact( + processing, ArtifactType.SOURCE_DOCUMENT, os.path.basename(processing.input_file.name), source.read() + ) diff --git a/manuscripts/utils/inspection.py b/manuscripts/utils/inspection.py new file mode 100644 index 0000000..74f053d --- /dev/null +++ b/manuscripts/utils/inspection.py @@ -0,0 +1,162 @@ +import os +import zipfile +from pathlib import PurePosixPath + +from lxml import etree + +from manuscripts.artifacts import save_artifact +from manuscripts.choices import ArtifactType, InputType, ProcessingAction, ProcessStatus +from manuscripts.utils.helpers import checksum_bytes, safe_archive_members +from manuscripts.utils.xml_utils import extract_article_metadata + +DOCUMENT_EXTENSIONS = {".docx"} +XML_EXTENSIONS = {".xml"} +ASSET_EXTENSIONS = { + ".avif", ".csv", ".eps", ".gif", ".jpeg", ".jpg", ".mml", ".png", + ".svg", ".tif", ".tiff", ".webp", +} + +ACTION_DEPENDENCIES = { + ProcessingAction.XML_GENERATION: [ProcessingAction.CITATION_MARKUP], + ProcessingAction.XML_VALIDATION: [], + ProcessingAction.SPS_PACKAGE_VALIDATION: [], + ProcessingAction.SPS_PACKAGE_GENERATION: [ProcessingAction.XML_VALIDATION], + ProcessingAction.HTML_GENERATION: [ProcessingAction.XML_VALIDATION], + ProcessingAction.PDF_GENERATION: [ProcessingAction.XML_VALIDATION], +} + +ACTION_ARTIFACT_TYPES = { + ProcessingAction.CITATION_MARKUP: [ArtifactType.MARKED_DOCUMENT], + ProcessingAction.XML_GENERATION: [ArtifactType.XML], + ProcessingAction.XML_VALIDATION: [ + ArtifactType.VALIDATION_REPORT, + ArtifactType.VALIDATION_EXCEPTIONS, + ], + ProcessingAction.SPS_PACKAGE_VALIDATION: [ + ArtifactType.VALIDATION_REPORT, + ArtifactType.VALIDATION_EXCEPTIONS, + ], + ProcessingAction.SPS_PACKAGE_GENERATION: [ArtifactType.SPS_PACKAGE], + ProcessingAction.HTML_GENERATION: [ArtifactType.HTML], + ProcessingAction.PDF_GENERATION: [ + ArtifactType.PDF, + ArtifactType.INTERMEDIATE_DOCUMENT, + ], +} + + +def inspect_input(path): + extension = os.path.splitext(path)[1].lower() + if extension in DOCUMENT_EXTENSIONS: + detected = InputType.DOCUMENT + contents = [{"path": os.path.basename(path), "kind": "document"}] + elif extension in XML_EXTENSIONS: + detected = InputType.XML + contents = [{"path": os.path.basename(path), "kind": "xml"}] + elif extension == ".zip": + detected, contents = inspect_zip(path) + else: + detected, contents = InputType.UNKNOWN, [] + return { + "detected_type": detected, + "contents": contents, + "suggested_actions": suggested_actions(detected), + } + + +def inspect_zip(path): + contents = [] + with zipfile.ZipFile(path) as archive: + for info in archive.infolist(): + if info.is_dir(): + continue + suffix = PurePosixPath(info.filename).suffix.lower() + if suffix in DOCUMENT_EXTENSIONS: + kind = "document" + elif suffix in XML_EXTENSIONS: + kind = "xml" + elif suffix in ASSET_EXTENSIONS: + kind = "asset" + else: + kind = "other" + contents.append({"path": info.filename, "kind": kind, "size": info.file_size}) + documents = [item for item in contents if item["kind"] == "document"] + xmls = [item for item in contents if item["kind"] == "xml"] + if len(documents) == 1 and not xmls: + detected = InputType.SOURCE_PACKAGE + elif xmls and not documents: + detected = InputType.SPS_PACKAGE + else: + detected = InputType.AMBIGUOUS_ZIP + return detected, contents + + +def suggested_actions(input_type): + if input_type in (InputType.DOCUMENT, InputType.SOURCE_PACKAGE): + return [ + ProcessingAction.CITATION_MARKUP, + ProcessingAction.XML_GENERATION, + ProcessingAction.XML_VALIDATION, + ProcessingAction.SPS_PACKAGE_GENERATION, + ProcessingAction.HTML_GENERATION, + ProcessingAction.PDF_GENERATION, + ] + if input_type == InputType.XML: + return [ + ProcessingAction.XML_VALIDATION, + ProcessingAction.SPS_PACKAGE_GENERATION, + ProcessingAction.HTML_GENERATION, + ProcessingAction.PDF_GENERATION, + ] + if input_type == InputType.SPS_PACKAGE: + return [ProcessingAction.SPS_PACKAGE_VALIDATION, ProcessingAction.XML_VALIDATION] + return [] + + +def resolve_actions(requested, input_type): + requested = list(dict.fromkeys(requested)) + applicable = set(suggested_actions(input_type)) + if input_type == InputType.SPS_PACKAGE: + applicable.update({ProcessingAction.HTML_GENERATION, ProcessingAction.PDF_GENERATION}) + selected = set(action for action in requested if action in applicable) + changed = True + while changed: + changed = False + for action in list(selected): + for dependency in ACTION_DEPENDENCIES.get(action, []): + if dependency in applicable and dependency not in selected: + selected.add(dependency) + changed = True + order = [value for value, _label in ProcessingAction.choices] + return [action for action in order if action in selected] + + +def inspect_processing(processing): + with processing.input_file.open("rb") as source: + content = source.read() + result = inspect_input(processing.input_file.path) + candidates = [] + try: + if result["detected_type"] == InputType.XML: + candidates.append(extract_article_metadata(content)) + elif result["detected_type"] == InputType.SPS_PACKAGE: + with zipfile.ZipFile(processing.input_file.path) as archive: + for member in safe_archive_members(archive): + if PurePosixPath(member.filename).suffix.lower() == ".xml": + candidates.append( + { + "path": member.filename, + **extract_article_metadata(archive.read(member)), + } + ) + except (ValueError, etree.XMLSyntaxError, zipfile.BadZipFile) as exc: + result["inspection_warning"] = str(exc) + result["article_candidates"] = candidates + processing.input_checksum = checksum_bytes(content) + processing.detected_type = result["detected_type"] + processing.inspection = result + processing.requested_actions = result["suggested_actions"] + processing.status = ProcessStatus.AWAITING_REVIEW + processing.save() + save_artifact(processing, ArtifactType.INPUT, os.path.basename(processing.input_file.name), content) + return processing diff --git a/manuscripts/utils/processing_actions.py b/manuscripts/utils/processing_actions.py new file mode 100644 index 0000000..dd9bfc8 --- /dev/null +++ b/manuscripts/utils/processing_actions.py @@ -0,0 +1,230 @@ +import io +import json +import logging +import os +import shutil +import time +import zipfile +from pathlib import PurePosixPath + +from django.conf import settings +from django.utils.text import slugify + +from ai.extract import extract_frontmatter +from docx_parser.parser import DocxParser +from labeling.fragments import plain_paragraph_text +from sps import utils as xml_utils +from sps.xref import is_marked, mark_references, validate_marks + +from manuscripts.artifacts import ( + article_asset_url_map, + article_assets_dir, + current_xml, + save_artifact, + save_path_artifact, +) +from manuscripts.choices import ArtifactType, ProcessingAction +from manuscripts.controller import event_complete, event_details_update, event_start +from manuscripts.models.article import ArticleArtifact +from manuscripts.structure import create_structure_version +from manuscripts.utils.docx_utils import extract_docx_structure +from manuscripts.utils.xml_utils import extract_article_metadata, generate_structure_xml + +logger = logging.getLogger(__name__) + + +def run_action(processing, action, task_id): + logger.info("Iniciando ação de processamento: %s para o processamento ID=%s", action, processing.pk) + started_action = time.monotonic() + + if action == ProcessingAction.SPS_PACKAGE_VALIDATION: + return _run_sps_package_validation(processing, action, task_id, started_action) + + partial = False + for article in processing.articles.all(): + logger.info("Executando ação %s no artigo ID=%s (%s)", action, article.pk, article) + started_article_action = time.monotonic() + event = event_start(processing, action, task_id, article) + + if action == ProcessingAction.CITATION_MARKUP: + _run_citation_markup(processing, article, event) + elif action == ProcessingAction.XML_GENERATION: + _run_xml_generation(processing, article, event) + elif action == ProcessingAction.XML_VALIDATION: + partial = _run_xml_validation(processing, article, event) or partial + elif action == ProcessingAction.SPS_PACKAGE_GENERATION: + _run_sps_package_generation(processing, article, event) + elif action == ProcessingAction.HTML_GENERATION: + _run_html_generation(processing, article, event) + elif action == ProcessingAction.PDF_GENERATION: + _run_pdf_generation(processing, article, event) + + logger.info("Ação %s concluída no artigo ID=%s em %.2fs", action, article.pk, time.monotonic() - started_article_action) + + logger.info("Concluída ação de processamento: %s para o processamento ID=%s em %.2fs", action, processing.pk, time.monotonic() - started_action) + return partial + + +def _run_sps_package_validation(processing, action, task_id, started_action): + event = event_start(processing, action, task_id) + logger.info("Executando validação do pacote SPS em %s...", processing.input_file.path) + rows, exceptions = xml_utils.validate_zip(processing.input_file.path) + save_artifact(processing, ArtifactType.VALIDATION_REPORT, "package.validation.json", json.dumps(rows, ensure_ascii=False).encode()) + save_artifact(processing, ArtifactType.VALIDATION_EXCEPTIONS, "package.exceptions.json", json.dumps(exceptions, ensure_ascii=False).encode()) + event_complete(event, "Pacote SPS validado.", {"issues": len(rows), "exceptions": len(exceptions)}) + elapsed = time.monotonic() - started_action + logger.info("Ação SPS_PACKAGE_VALIDATION concluída em %.2fs. Problemas: %d, Exceções: %d", elapsed, len(rows), len(exceptions)) + return bool(rows or exceptions) + + +def _run_citation_markup(processing, article, event): + source = ArticleArtifact.objects.filter(processing=processing, artifact_type=ArtifactType.SOURCE_DOCUMENT, is_current=True).first() + logger.info("Abrindo arquivo DOCX do artigo: %s", source.file.path) + document = DocxParser.open_docx(source.file.path) + if not is_marked(document): + document = mark_references(document) + validation = validate_marks(document) + buffer = io.BytesIO() + document.save(buffer) + save_artifact(processing, ArtifactType.MARKED_DOCUMENT, f"{slugify(str(article))}-marked.docx", buffer.getvalue(), article, validation) + + front, body, back, xref_status = extract_docx_structure(document, source.file.path) + event_details_update( + event, + {"frontmatter_ai": {"status": "running", "provider": "pending", "message": "Extraindo metadados estruturais com IA."}}, + ) + front, article_updates, frontmatter_warnings, frontmatter_ai = extract_frontmatter( + article, front, body, docx_path=source.file.path + ) + event_details_update(event, {"frontmatter_ai": frontmatter_ai}) + if article_updates: + for field, value in article_updates.items(): + setattr(article, field, value) + article.save(update_fields=[*article_updates.keys(), "updated"]) + + structure = create_structure_version( + article, processing, processing.confirmed_type, front, body, back, + warnings=frontmatter_warnings, xref_status=xref_status, + ) + _ensure_article_title(article, front) + + event_complete( + event, + "Estrutura extraída e citações marcadas.", + { + **validation, + "structure_version": structure.version, + "body_blocks": len(body), + "references": structure.references.count(), + "citations": structure.citations.count(), + "frontmatter_ai": frontmatter_ai, + "frontmatter_ai_warnings": frontmatter_warnings, + }, + ) + + +def _ensure_article_title(article, front): + if article.title or not front: + return + title_val = "" + for item in front: + type_, value = _unwrap_item(item) + if type_ in ("paragraph", "paragraph_with_language") and value.get("label") == "<article-title>": + title_val = plain_paragraph_text(value.get("paragraph")) + break + if not title_val: + first = front[0] + _, first_value = _unwrap_item(first) + title_val = plain_paragraph_text(first_value.get("paragraph", "")) + if title_val: + article.title = title_val + article.save(update_fields=["title", "updated"]) + + +def _unwrap_item(item): + if hasattr(item, "block_type") and hasattr(item, "value"): + return item.block_type, item.value + if isinstance(item, dict): + return item.get("type"), item.get("value") or {} + return None, {} + + +def _run_xml_generation(processing, article, event): + structure = article.current_structure + if not structure: + raise ValueError(f"Artigo {article} não possui estrutura para gerar XML.") + save_artifact( + processing, ArtifactType.XML, f"{slugify(str(article))}.xml", + generate_structure_xml(structure), article, structure=structure, + ) + event_complete(event, "XML gerado.") + + +def _run_xml_validation(processing, article, event): + xml = current_xml(processing, article) + validation_path, exceptions_path = xml_utils.validate_xml_document( + xml.file.path, os.path.join(settings.MEDIA_ROOT, "processings", "validation"), {} + ) + save_path_artifact(processing, ArtifactType.VALIDATION_REPORT, validation_path, article, structure=xml.structure) + exceptions = save_path_artifact(processing, ArtifactType.VALIDATION_EXCEPTIONS, exceptions_path, article, structure=xml.structure) + with open(validation_path, encoding="utf-8") as validation_file: + has_report_rows = sum(1 for _line in validation_file) > 1 + with open(exceptions_path, encoding="utf-8") as exceptions_file: + has_exceptions = bool(exceptions_file.read().strip()) + has_issues = has_report_rows or has_exceptions + event_complete(event, "XML validado.", {"has_issues": has_issues}) + return has_issues + + +def _run_sps_package_generation(processing, article, event): + xml = current_xml(processing, article) + with xml.file.open("rb") as xml_source: + referenced_assets = { + PurePosixPath(value.split("?", 1)[0]).name + for value in extract_article_metadata(xml_source.read()).get("assets", []) + } + buffer = io.BytesIO() + with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive: + archive.write(xml.file.path, os.path.basename(xml.file.name)) + for asset in ArticleArtifact.objects.filter(processing=processing, article=article, artifact_type=ArtifactType.ASSET, is_current=True): + if PurePosixPath(asset.original_path or asset.file.name).name not in referenced_assets: + continue + archive.write(asset.file.path, asset.original_path or os.path.basename(asset.file.name)) + save_artifact( + processing, ArtifactType.SPS_PACKAGE, f"{slugify(str(article))}.zip", + buffer.getvalue(), article, structure=xml.structure, + ) + event_complete(event, "Pacote SPS gerado.") + + +def _run_html_generation(processing, article, event): + xml = current_xml(processing, article) + path, language = xml_utils.generate_html_for_xml_document( + xml.file.path, + os.path.join(settings.MEDIA_ROOT, "processings", "html"), + settings.HTML_GENERATION_CONFIG, + asset_url_map=article_asset_url_map(processing, article), + ) + save_path_artifact(processing, ArtifactType.HTML, path, article, {"language": language}, structure=xml.structure) + event_complete(event, "HTML gerado.") + + +def _run_pdf_generation(processing, article, event): + xml = current_xml(processing, article) + tmp_assets_dir = article_assets_dir(processing, article) + try: + pdf_params = { + "assets_dir": tmp_assets_dir, + "html_config": settings.HTML_GENERATION_CONFIG, + "asset_url_map": article_asset_url_map(processing, article), + } + pdf_path, docx_path, language = xml_utils.generate_pdf_for_xml_document( + xml.file.path, os.path.join(settings.MEDIA_ROOT, "processings", "pdf"), pdf_params, + ) + finally: + if tmp_assets_dir and os.path.isdir(tmp_assets_dir): + shutil.rmtree(tmp_assets_dir, ignore_errors=True) + save_path_artifact(processing, ArtifactType.PDF, pdf_path, article, {"language": language}, structure=xml.structure) + if docx_path: + save_path_artifact(processing, ArtifactType.INTERMEDIATE_DOCUMENT, docx_path, article, {"language": language}, structure=xml.structure) + event_complete(event, "PDF gerado.") diff --git a/manuscripts/utils/xml_utils.py b/manuscripts/utils/xml_utils.py new file mode 100644 index 0000000..a8cdeb3 --- /dev/null +++ b/manuscripts/utils/xml_utils.py @@ -0,0 +1,307 @@ +import re +from types import SimpleNamespace + +from lxml import etree + +from sps.xml import get_xml + +from manuscripts.utils.helpers import ( + _block, + _raw_reference, + to_dict_list, +) + + +def _plain_text(node): + return " ".join("".join(node.itertext()).split()) + + +def _get_inner_xml(node): + xml_str = etree.tostring(node, encoding="unicode", with_tail=False) + xml_str = re.sub(r'\s*xmlns(:[a-zA-Z0-9]+)?="[^"]+"', '', xml_str) + match = re.match(r'^<p(?:\s+[^>]*)?>(.*)</p>$', xml_str, re.DOTALL) + if match: + return match.group(1).strip() + return xml_str.strip() + + +def parse_xml_structure(xml_content): + root = etree.fromstring(xml_content) + front = [] + + title = root.xpath(".//article-meta/title-group/article-title[1]") + if title: + front.append({ + "type": "paragraph_with_language", + "value": { + "label": "<article-title>", + "language": root.get("{http://www.w3.org/XML/1998/namespace}lang", "en"), + "paragraph": _plain_text(title[0]) + } + }) + + for trans_title in root.xpath(".//article-meta/title-group/trans-title-group"): + lang = trans_title.get("{http://www.w3.org/XML/1998/namespace}lang") + title_text = trans_title.xpath("string(./trans-title)") + if title_text: + front.append({ + "type": "paragraph_with_language", + "value": { + "label": "<trans-title>", + "language": lang, + "paragraph": title_text.strip() + } + }) + + for contrib in root.xpath(".//article-meta/contrib-group/contrib[@contrib-type='author']"): + surname = contrib.xpath("string(./name/surname)") + given_names = contrib.xpath("string(./name/given-names)") + orcid = contrib.xpath("string(./contrib-id[@contrib-id-type='orcid'])") + xrefs = contrib.xpath("./xref[@ref-type='aff']") + affids = ",".join(x.get("rid", "").replace("aff", "") for x in xrefs) + char = xrefs[0].text if xrefs else "" + front.append({ + "type": "author_paragraph", + "value": { + "label": "<contrib>", + "paragraph": f"{given_names} {surname}".strip(), + "surname": surname, + "given_names": given_names, + "orcid": orcid, + "affid": affids, + "char": char, + } + }) + + for aff in root.xpath(".//article-meta/aff"): + affid = aff.get("id", "").replace("aff", "") + char = aff.xpath("string(./label)") + orgname = aff.xpath("string(./institution[@content-type='orgname'])") + orgdiv1 = aff.xpath("string(./institution[@content-type='orgdiv1'])") + orgdiv2 = aff.xpath("string(./institution[@content-type='orgdiv2'])") + city = aff.xpath("string(.//city)") + state = aff.xpath("string(.//state)") + country = aff.xpath("string(.//country)") + code_country = aff.xpath("string(.//country/@country)") + original = _plain_text(aff) + front.append({ + "type": "aff_paragraph", + "value": { + "label": "<aff>", + "paragraph": original, + "affid": affid, + "char": char, + "orgname": orgname, + "orgdiv1": orgdiv1, + "orgdiv2": orgdiv2, + "city": city, + "state": state, + "country": country, + "code_country": code_country, + "original": original, + } + }) + + for abstract in root.xpath(".//article-meta/abstract | .//article-meta/trans-abstract"): + label = "<abstract>" + lang = abstract.get("{http://www.w3.org/XML/1998/namespace}lang") or root.get("{http://www.w3.org/XML/1998/namespace}lang", "en") + title_node = abstract.xpath("./title[1]") + title_text = title_node[0].text if title_node else "Abstract" + front.append({ + "type": "paragraph", + "value": { + "label": "<abstract-title>", + "paragraph": title_text + } + }) + for p in abstract.xpath("./p"): + front.append({ + "type": "paragraph_with_language", + "value": { + "label": label, + "language": lang, + "paragraph": _get_inner_xml(p) + } + }) + + for kwd_group in root.xpath(".//article-meta/kwd-group"): + lang = kwd_group.get("{http://www.w3.org/XML/1998/namespace}lang") + title_node = kwd_group.xpath("./title[1]") + title_text = title_node[0].text if title_node else "Keywords" + front.append({ + "type": "paragraph", + "value": { + "label": "<kwd-title>", + "paragraph": title_text + } + }) + kwds = ", ".join(k.text for k in kwd_group.xpath("./kwd") if k.text) + front.append({ + "type": "paragraph_with_language", + "value": { + "label": "<kwd-group>", + "language": lang, + "paragraph": kwds + } + }) + + body = [] + for node in root.xpath("./body//*"): + if node.tag == "title": + body.append(_block("<sec>", _plain_text(node))) + elif node.tag == "p": + body.append(_block("<p>", _get_inner_xml(node))) + + back = [] + for position, node in enumerate(root.xpath(".//ref-list/ref"), 1): + ref_id = node.get("id") or f"B{position}" + text = _plain_text(node) + block = _raw_reference(position, text) + block["value"]["refid"] = ref_id + back.append(block) + + warnings = [] + supported = {"article", "front", "journal-meta", "article-meta", "title-group", "article-title", + "body", "back", "ref-list", "ref", "mixed-citation", "element-citation", + "sec", "title", "p", "xref", "italic", "bold", "sub", "sup"} + unsupported = sorted({etree.QName(node).localname for node in root.iter()} - supported) + if unsupported: + warnings.append( + {"code": "UNMODELED_XML_NODES", "nodes": unsupported, "message": "Nós preservados somente no XML-base."} + ) + return front, body, back, warnings + + +def extract_article_metadata(xml_content): + tree = etree.fromstring(xml_content) + title_nodes = tree.xpath(".//article-meta/title-group/article-title") + title = " ".join(title_nodes[0].itertext()).strip() if title_nodes else "" + doi = tree.xpath("string(.//article-id[@pub-id-type='doi'][1])").strip().lower() + pid = ( + tree.xpath("string(.//article-id[@pub-id-type='publisher-id'][1])").strip() + or tree.xpath("string(.//article-id[@pub-id-type='other'][1])").strip() + ) + href_values = tree.xpath("//@*[local-name()='href']") + assets = [value for value in href_values if value and not value.startswith("#")] + + namespaces = { + "xlink": "http://www.w3.org/1999/xlink", + "xml": "http://www.w3.org/XML/1998/namespace", + } + language = tree.xpath("string(./@xml:lang)", namespaces=namespaces) or "en" + license_url = tree.xpath("string(.//article-meta/permissions/license/@xlink:href)", namespaces=namespaces) or tree.xpath("string(.//article-meta/permissions/license/@href)") + elocatid = tree.xpath("string(.//article-meta/elocation-id[1])") + fpage = tree.xpath("string(.//article-meta/fpage[1])") + lpage = tree.xpath("string(.//article-meta/lpage[1])") + seq = tree.xpath("string(.//article-meta/fpage[1]/@seq)") + + artdate_str = None + pub_date_node = tree.xpath(".//article-meta/pub-date[@date-type='pub']") or tree.xpath(".//article-meta/pub-date") + if pub_date_node: + day = pub_date_node[0].xpath("string(day)").strip() + month = pub_date_node[0].xpath("string(month)").strip() + year = pub_date_node[0].xpath("string(year)").strip() + if year: + month_str = month.zfill(2) if month else "01" + day_str = day.zfill(2) if day else "01" + if not month_str.isdigit() or int(month_str) < 1 or int(month_str) > 12: + month_str = "01" + if not day_str.isdigit() or int(day_str) < 1 or int(day_str) > 31: + day_str = "01" + artdate_str = f"{year}-{month_str}-{day_str}" + + return { + "title": title, + "doi": doi, + "pid": pid, + "assets": assets, + "language": language, + "license": license_url, + "elocatid": elocatid, + "fpage": fpage, + "lpage": lpage, + "seq": seq, + "artdate": artdate_str, + } + + +def _article_adapter(article): + journal = article.journal + return SimpleNamespace( + title=article.title, + content=[], + acronym=(journal.acronym if journal else ""), + title_nlm=(journal.title_nlm if journal else ""), + journal_title=(journal.title if journal else ""), + short_title=(journal.short_title if journal else ""), + pissn=(journal.pissn if journal else ""), + eissn=(journal.eissn if journal else ""), + pubname=(journal.publisher_name if journal else ""), + issue=article.issue, + language=article.language or "en", + license=article.license or "", + artdate=article.artdate, + ahpdate=article.ahpdate, + elocatid=article.elocatid or "", + fpage=article.fpage or "", + lpage=article.lpage or "", + seq=article.seq or "", + dateiso=article.artdate.strftime("%Y-%m-%d") if article.artdate else "", + ) + + +def generate_structure_xml(structure): + front_raw = to_dict_list(structure.front) + body_raw = to_dict_list(structure.body) + back_raw = to_dict_list(structure.back) + + xml, normalized_body = get_xml( + _article_adapter(structure.article), + front_raw, + body_raw, + back_raw, + ) + if normalized_body != body_raw: + structure.body = normalized_body + structure.save(update_fields=["body", "updated"]) + if structure.base_xml: + base = etree.fromstring(structure.base_xml.encode("utf-8")) + generated = etree.fromstring(xml.encode("utf-8")) + + xpaths_to_sync = ( + "./body", + "./back/ref-list", + ".//article-meta/title-group/article-title", + ".//article-meta/contrib-group", + ".//article-meta/aff", + ".//article-meta/abstract", + ".//article-meta/trans-abstract", + ".//article-meta/kwd-group", + ".//article-meta/permissions", + ) + for xpath in xpaths_to_sync: + sources = generated.xpath(xpath) + targets = base.xpath(xpath) + + if not sources: + for t in targets: + t.getparent().remove(t) + continue + + if targets: + first_target = targets[0] + parent = first_target.getparent() + index = parent.index(first_target) + for t in targets: + parent.remove(t) + for i, s in enumerate(sources): + parent.insert(index + i, s) + else: + parent_xpath = xpath.rsplit("/", 1)[0] + parent_targets = base.xpath(parent_xpath) + if parent_targets: + parent = parent_targets[0] + for s in sources: + parent.append(s) + return etree.tostring(base, encoding="utf-8", xml_declaration=True) + return xml.encode("utf-8") diff --git a/manuscripts/views/__init__.py b/manuscripts/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/manuscripts/views/autocomplete.py b/manuscripts/views/autocomplete.py new file mode 100644 index 0000000..c75343f --- /dev/null +++ b/manuscripts/views/autocomplete.py @@ -0,0 +1,55 @@ +from http import HTTPStatus +from urllib.parse import unquote + +from django.apps import apps +from django.db.models import Q +from django.http import HttpResponseBadRequest, JsonResponse +from django.urls import re_path +from django.views.decorators.http import require_POST +from wagtail.admin.auth import require_admin_access +from wagtailautocomplete.views import create, objects, render_page +from wagtailautocomplete.views import search as default_search + + +@require_POST +def search(request): + target_model = request.POST.get("type", "wagtailcore.Page") + is_article_issue_filter = request.POST.get("article_issue_filter") == "1" + if target_model != "journals.Issue" or not is_article_issue_filter: + return default_search(request) + + journal_id = request.POST.get("journal_id") + if not journal_id: + return JsonResponse({"items": []}) + + try: + limit = int(request.POST.get("limit", 100)) + model = apps.get_model(target_model) + except (LookupError, ValueError): + return HttpResponseBadRequest() + + search_query = request.POST.get("query", "") + queryset = model.objects.filter(journal_id=journal_id) + if search_query: + queryset = queryset.filter( + Q(number__icontains=search_query) + | Q(volume__icontains=search_query) + | Q(year__icontains=search_query) + | Q(supplement__icontains=search_query) + | Q(journal__title__icontains=search_query) + ) + + exclude = request.POST.get("exclude", "") + if exclude: + exclusions = [unquote(item) for item in exclude.split(",") if item] + queryset = queryset.exclude(pk__in=exclusions) + + results = map(render_page, queryset.order_by("volume", "number", "year")[:limit]) + return JsonResponse({"items": list(results)}, status=HTTPStatus.OK) + + +urlpatterns = [ + re_path(r"^create/", require_admin_access(create)), + re_path(r"^objects/", require_admin_access(objects)), + re_path(r"^search/", require_admin_access(search)), +] diff --git a/manuscripts/views/editorial.py b/manuscripts/views/editorial.py new file mode 100644 index 0000000..0690c9f --- /dev/null +++ b/manuscripts/views/editorial.py @@ -0,0 +1,551 @@ +import copy +import csv +import io +import json +import mimetypes +import re + +from django.contrib import messages +from django.contrib.admin.views.decorators import staff_member_required +from django.db import transaction +from django.http import FileResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.utils.translation import gettext_lazy as _ +from django.views.decorators.http import require_POST +from lxml import etree +import packtools +from wagtail.admin.panels import ObjectList + +from manuscripts.artifacts import save_artifact +from manuscripts.choices import ArtifactType, InputType, ProcessingAction, ProcessStatus +from manuscripts.forms import ProcessingReviewForm +from manuscripts.models.article import ( + Article, + ArticleArtifact, + ArticleReference, + ArticleStructureVersion, + CitationOccurrence, +) +from manuscripts.models.processing import Processing +from manuscripts.structure import create_structure_version +from manuscripts.utils.helpers import to_dict_list +from manuscripts.utils.xml_utils import generate_structure_xml +from manuscripts.tasks import process_input + +GENERATED_ARTIFACT_TYPES = [ + ArtifactType.MARKED_DOCUMENT, + ArtifactType.STRUCTURE, + ArtifactType.XML, + ArtifactType.ASSET, + ArtifactType.SPS_PACKAGE, + ArtifactType.VALIDATION_REPORT, + ArtifactType.VALIDATION_EXCEPTIONS, + ArtifactType.HTML, + ArtifactType.PDF, + ArtifactType.INTERMEDIATE_DOCUMENT, +] + + +def _delete_artifacts(artifacts): + deleted = 0 + for artifact in list(artifacts): + file_field = artifact.file + artifact.delete() + if file_field: + file_field.delete(save=False) + deleted += 1 + return deleted + + +def _confirmed_cleanup(request): + return request.POST.get("confirmation", "").strip().upper() == "LIMPAR" + + +def _ensure_current_structure(article): + if article.structure_versions.filter(is_current=True).exists(): + return True + latest = article.structure_versions.order_by("-version").first() + if not latest: + return False + latest.is_current = True + latest.save(update_fields=["is_current", "updated"]) + return True + + +def _save_corrected_structure(article, structure, front, body, back): + front_raw = to_dict_list(front) + body_raw = to_dict_list(body) + back_raw = to_dict_list(back) + new_structure = create_structure_version( + article, + structure.processing, + InputType.DOCUMENT, + front_raw, + body_raw, + back_raw, + base_xml=structure.base_xml, + warnings=structure.roundtrip_warnings, + xref_status=structure.xref_status, + ) + article.artifacts.filter( + artifact_type__in=[ + ArtifactType.XML, + ArtifactType.VALIDATION_REPORT, + ArtifactType.VALIDATION_EXCEPTIONS, + ArtifactType.SPS_PACKAGE, + ArtifactType.HTML, + ArtifactType.PDF, + ], + is_current=True, + ).update(is_stale=True) + save_artifact( + new_structure.processing, + ArtifactType.XML, + f"article-{article.pk}.xml", + generate_structure_xml(new_structure), + article, + structure=new_structure, + ) + return new_structure + + +@staff_member_required +def processing_review(request, pk): + processing = get_object_or_404(Processing, pk=pk) + if request.method == "POST": + form = ProcessingReviewForm(request.POST, instance=processing) + if form.is_valid(): + processing = form.save(commit=False) + processing.confirmed_type = processing.detected_type + processing.status = ProcessStatus.PENDING + processing.save() + process_input.delay(processing.pk) + messages.success(request, _("Processing started.")) + return redirect("wagtailsnippets_manuscripts_processing:inspect", pk=pk) + else: + form = ProcessingReviewForm(instance=processing, initial={ + "requested_actions": processing.requested_actions, + }) + return render( + request, + "manuscripts/processing_review.html", + {"processing": processing, "form": form}, + ) + + +@staff_member_required +@require_POST +def processing_cancel(request, pk): + processing = get_object_or_404(Processing, pk=pk) + processing.status = ProcessStatus.CANCELLED + processing.save(update_fields=["status", "updated"]) + messages.success(request, _("Processing cancelled.")) + return redirect("wagtailsnippets_manuscripts_processing:inspect", pk=pk) + + +@staff_member_required +@require_POST +def processing_reprocess(request, pk): + processing = get_object_or_404(Processing, pk=pk) + action = request.POST.get("action") + if action not in ProcessingAction.values: + action = None + processing.retry_count += 1 + processing.save(update_fields=["retry_count", "updated"]) + process_input.delay(processing.pk, start_action=action) + messages.success(request, _("Reprocessing started.")) + return redirect("wagtailsnippets_manuscripts_processing:inspect", pk=pk) + + +@staff_member_required +@require_POST +def processing_cleanup_artifacts(request, pk): + processing = get_object_or_404(Processing, pk=pk) + if not _confirmed_cleanup(request): + messages.error(request, _('Enter "LIMPAR" to confirm cleanup.')) + return redirect("wagtailsnippets_manuscripts_processing:inspect", pk=pk) + + include_input = request.POST.get("include_input") == "1" + artifact_types = None if include_input else GENERATED_ARTIFACT_TYPES + artifacts = processing.artifacts.all() + if artifact_types is not None: + artifacts = artifacts.filter(artifact_type__in=artifact_types) + + with transaction.atomic(): + deleted = _delete_artifacts(artifacts) + article_ids = list(processing.articles.values_list("pk", flat=True)) + deleted_structures = ArticleStructureVersion.objects.filter(processing=processing).count() + ArticleStructureVersion.objects.filter(processing=processing).delete() + processing.events.all().delete() + for article in Article.objects.filter(pk__in=article_ids): + if not _ensure_current_structure(article): + article.status = ProcessStatus.PENDING + article.save(update_fields=["status", "updated"]) + if include_input and processing.input_file: + input_file = processing.input_file + processing.status = ProcessStatus.CANCELLED + processing.current_action = "" + processing.save(update_fields=["status", "current_action", "updated"]) + input_file.delete(save=False) + else: + processing.status = ProcessStatus.AWAITING_REVIEW + processing.current_action = "" + processing.error_message = "" + processing.error_details = {} + processing.error_traceback = "" + processing.save( + update_fields=[ + "status", + "current_action", + "error_message", + "error_details", + "error_traceback", + "updated", + ] + ) + + messages.success( + request, + _("Cleanup concluded: %(artifacts)s artifact(s) and %(structures)s structural version(s) removed.") + % {"artifacts": deleted, "structures": deleted_structures}, + ) + return redirect("wagtailsnippets_manuscripts_processing:inspect", pk=pk) + + +@staff_member_required +@require_POST +def article_cleanup_artifacts(request, pk): + article = get_object_or_404(Article, pk=pk) + if not _confirmed_cleanup(request): + messages.error(request, _('Enter "LIMPAR" to confirm cleanup.')) + return redirect("wagtailsnippets_manuscripts_article:inspect", pk=pk) + + with transaction.atomic(): + deleted_artifacts = _delete_artifacts(article.artifacts.all()) + deleted_structures = article.structure_versions.count() + article.structure_versions.all().delete() + article.events.all().delete() + article.status = ProcessStatus.PENDING + article.save(update_fields=["status", "updated"]) + + messages.success( + request, + _("Cleanup concluded: %(artifacts)s artifact(s) and %(structures)s structural version(s) removed.") + % {"artifacts": deleted_artifacts, "structures": deleted_structures}, + ) + return redirect("wagtailsnippets_manuscripts_article:inspect", pk=pk) + + +@staff_member_required +def artifact_download(request, pk): + artifact = get_object_or_404(ArticleArtifact, pk=pk) + return FileResponse( + artifact.file.open("rb"), + as_attachment=True, + filename=artifact.file.name.rsplit("/", 1)[-1], + ) + + +@staff_member_required +def artifact_preview(request, pk): + """Serve o artefato inline no browser (sem download). Ideal para preview de HTML.""" + artifact = get_object_or_404(ArticleArtifact, pk=pk) + filename = artifact.file.name.rsplit("/", 1)[-1] + content_type, _ = mimetypes.guess_type(filename) + content_type = content_type or "application/octet-stream" + + return FileResponse( + artifact.file.open("rb"), + as_attachment=False, + content_type=content_type, + ) + + +@staff_member_required +def article_validation_view(request, pk): + """Renderiza o relatório de validação (JSON) como tabela no admin.""" + article = get_object_or_404(Article, pk=pk) + report_artifact = article.current_artifact(ArtifactType.VALIDATION_REPORT) + exceptions_artifact = article.current_artifact(ArtifactType.VALIDATION_EXCEPTIONS) + xml_artifact = article.current_artifact(ArtifactType.XML) + + rows = [] + exceptions = [] + + if report_artifact: + try: + content = report_artifact.file.open("r").read() + if content.strip().startswith("[") or content.strip().startswith("{"): + try: + rows = json.loads(content) + if not isinstance(rows, list): + rows = [rows] + except json.JSONDecodeError: + rows = [json.loads(line) for line in content.splitlines() if line.strip()] + else: + f = io.StringIO(content) + reader = csv.DictReader(f) + rows = [] + for r in reader: + rows.append({ + "group": r.get("context", "—"), + "title": "—", + "attribute": "—", + "validation_type": "—", + "response": r.get("response", "—"), + "expected_value": "—", + "got_value": r.get("detail", "—"), + "advice": r.get("advice", ""), + }) + except Exception: + rows = [] + + if exceptions_artifact: + try: + content = exceptions_artifact.file.open("r").read() + try: + exceptions = json.loads(content) + if not isinstance(exceptions, list): + exceptions = [exceptions] + except json.JSONDecodeError: + exceptions = [json.loads(line) for line in content.splitlines() if line.strip()] + except Exception: + exceptions = [] + + # Validação de schema DTD/SPS via packtools.XMLValidator + schema_valid = None + schema_errors = [] + annotated_xml = None + + if xml_artifact: + try: + validator = packtools.XMLValidator.parse(xml_artifact.file.path) + schema_valid, _errors = validator.validate_all() + schema_errors = [ + {"message": e.message, "line": getattr(e, "line", None)} + for e in _errors + ] + err_tree = validator.annotate_errors() + annotated_xml = etree.tostring( + err_tree, + pretty_print=True, + encoding="unicode", + xml_declaration=False, + ) + except Exception as exc: + schema_valid = None + annotated_xml = None + schema_errors = [{"message": str(exc), "line": None}] + + return render( + request, + "manuscripts/article_validation.html", + { + "article": article, + "rows": rows, + "exceptions": exceptions, + "report_artifact": report_artifact, + "exceptions_artifact": exceptions_artifact, + "xml_artifact": xml_artifact, + "schema_valid": schema_valid, + "schema_errors": schema_errors, + "annotated_xml": annotated_xml, + }, + ) + + + +@staff_member_required +def article_structure_edit(request, pk): + article = get_object_or_404(Article, pk=pk) + + version_num = request.GET.get("version") + if version_num: + structure = get_object_or_404(ArticleStructureVersion, article=article, version=version_num) + else: + structure = article.current_structure + + if not structure: + messages.error(request, _("The article does not yet have an editable structure.")) + return redirect("wagtailsnippets_manuscripts_article:inspect", pk=pk) + + edit_handler = ObjectList(ArticleStructureVersion.panels).bind_to_model(ArticleStructureVersion) + form_class = edit_handler.get_form_class() + + if request.method == "POST": + form = form_class(request.POST, request.FILES, instance=structure) + if form.is_valid(): + _save_corrected_structure( + article, + structure, + form.cleaned_data["front"], + form.cleaned_data["body"], + form.cleaned_data["back"], + ) + messages.success(request, _("New structural version saved and XML regenerated.")) + return redirect("wagtailsnippets_manuscripts_article:inspect", pk=pk) + else: + form = form_class(instance=structure) + + bound_edit_handler = edit_handler.get_bound_panel( + instance=structure, form=form, request=request + ) + + xml_artifact = article.current_artifact(ArtifactType.XML) + + versions = article.structure_versions.order_by("-version") + current_structure = article.current_structure + + return render( + request, + "manuscripts/article_structure_edit.html", + { + "article": article, + "structure": structure, + "form": form, + "edit_handler": bound_edit_handler, + "xml_artifact": xml_artifact, + "versions": versions, + "current_structure": current_structure, + "is_historical_version": bool(current_structure and structure.pk != current_structure.pk), + }, + ) + + +@staff_member_required +@require_POST +def article_structure_revert(request, pk, version): + article = get_object_or_404(Article, pk=pk) + target = get_object_or_404(ArticleStructureVersion, article=article, version=version) + + new_structure = _save_corrected_structure( + article, + target, + target.front, + target.body, + target.back, + ) + + messages.success( + request, + _("Structure reverted to version %(version)s. New version %(new_version)s created.") % { + "version": version, + "new_version": new_structure.version, + } + ) + return redirect("manuscripts:article_structure_edit", pk=pk) + + +@staff_member_required +def article_references_edit(request, pk): + article = get_object_or_404(Article, pk=pk) + structure = article.current_structure + if not structure: + messages.error(request, _("The article does not yet have an editable structure.")) + return redirect("wagtailsnippets_manuscripts_article:inspect", pk=pk) + + references = structure.references.select_related( + "reference", "selected_element" + ).prefetch_related("reference__element_citation") + citations = structure.citations.prefetch_related("references") + + return render( + request, + "manuscripts/article_references_edit.html", + { + "article": article, + "structure": structure, + "references": references, + "citations": citations, + }, + ) + + +@staff_member_required +@require_POST +def citation_update(request, pk): + citation = get_object_or_404(CitationOccurrence, pk=pk) + references = citation.structure.references.filter(pk__in=request.POST.getlist("references")) + structure = citation.structure + body_raw = to_dict_list(structure.body) + body = copy.deepcopy(body_raw) + block_index = citation.location.get("block") + if block_index is not None and block_index < len(body): + paragraph = body[block_index].get("value", {}).get("paragraph", "") + rid = " ".join(references.values_list("ref_id", flat=True)) + replacement = ( + f'<xref ref-type="bibr" rid="{rid}">{citation.text}</xref>' + if rid + else citation.text + ) + paragraph = re.sub( + rf'<xref[^>]*>{re.escape(citation.text)}</xref>', + replacement, + paragraph, + count=1, + ) + body[block_index]["value"]["paragraph"] = paragraph + front_raw = to_dict_list(structure.front) + back_raw = to_dict_list(structure.back) + _save_corrected_structure( + structure.article, structure, copy.deepcopy(front_raw), body, copy.deepcopy(back_raw) + ) + messages.success(request, _("Link updated in a new structural version.")) + return redirect("manuscripts:article_references_edit", pk=structure.article_id) + + +@staff_member_required +@require_POST +def article_reference_select(request, pk): + article_reference = get_object_or_404(ArticleReference, pk=pk) + selected_element = request.POST.get("selected_element") + candidate = None + if selected_element: + candidate = article_reference.reference.element_citation.filter( + pk=selected_element + ).first() + structure = article_reference.structure + back_raw = to_dict_list(structure.back) + back = copy.deepcopy(back_raw) + if candidate: + for block in back: + value = block.get("value", {}) + if value.get("refid") == article_reference.ref_id: + value.update(candidate.marked) + value["refid"] = article_reference.ref_id + value["label"] = "<p>" + value["paragraph"] = article_reference.mixed_citation + break + front_raw = to_dict_list(structure.front) + body_raw = to_dict_list(structure.body) + new_structure = _save_corrected_structure( + structure.article, structure, copy.deepcopy(front_raw), copy.deepcopy(body_raw), back + ) + new_reference = new_structure.references.get(ref_id=article_reference.ref_id) + new_reference.selected_element = candidate + new_reference.save(update_fields=["selected_element", "updated"]) + messages.success(request, _("Interpretation updated in a new structural version.")) + return redirect( + "manuscripts:article_references_edit", + pk=article_reference.structure.article_id, + ) + + +@staff_member_required +@require_POST +def article_reprocess(request, pk): + article = get_object_or_404(Article, pk=pk) + processing = article.processings.order_by("-created").first() + if not processing: + messages.error(request, _("This article has no associated processings.")) + return redirect("wagtailsnippets_manuscripts_article:inspect", pk=pk) + + action = request.POST.get("action") + if action not in ProcessingAction.values: + action = None + + processing.retry_count += 1 + processing.save(update_fields=["retry_count", "updated"]) + process_input.delay(processing.pk, start_action=action) + messages.success(request, _("Reprocessing started.")) + return redirect("wagtailsnippets_manuscripts_article:inspect", pk=pk) diff --git a/manuscripts/views/wagtail.py b/manuscripts/views/wagtail.py new file mode 100644 index 0000000..2872890 --- /dev/null +++ b/manuscripts/views/wagtail.py @@ -0,0 +1,159 @@ +from pathlib import PurePosixPath + +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from wagtail.snippets.views.snippets import CreateView, InspectView + +from manuscripts.choices import ArtifactType, ProcessingAction +from manuscripts.forms import ( + DOCXUploadForm, + ProcessingUploadForm, + SPSPackageUploadForm, + XMLUploadForm, +) +from manuscripts.utils.inspection import inspect_processing + + +class ProcessingCreateView(CreateView): + page_title = _("Upload file") + submit_button_label = _("Upload and review") + + def get_form_class(self): + return ProcessingUploadForm + + def save_instance(self): + self.form.instance.creator = self.request.user + processing = super().save_instance() + inspect_processing(processing) + return processing + + def get_success_url(self): + return reverse("manuscripts:processing_review", args=[self.object.pk]) + + +class XMLImportCreateView(ProcessingCreateView): + page_title = _("XML") + + def get_form_class(self): + return XMLUploadForm + + +class ArticleInputCreateView(ProcessingCreateView): + page_title = _("DOCX") + + def get_form_class(self): + return DOCXUploadForm + + +class SPSPackageImportCreateView(ProcessingCreateView): + page_title = _("SPS Package") + + def get_form_class(self): + return SPSPackageUploadForm + + +class OwnedCreateView(CreateView): + def save_instance(self): + self.form.instance.creator = self.request.user + return super().save_instance() + + +class ProcessingInspectView(InspectView): + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["events"] = self.object.events.select_related("article").all() + artifacts = self.object.artifacts.select_related("article").all() + context["artifacts"] = artifacts + context["artifact_rows"] = [ + { + "artifact": artifact, + "filename": PurePosixPath(artifact.file.name).name, + } + for artifact in artifacts + ] + context["articles"] = self.object.articles.all() + context["action_choices"] = ProcessingAction.choices + return context + + +class ArticleInspectView(InspectView): + artifact_order = { + ArtifactType.XML: 10, + ArtifactType.HTML: 20, + ArtifactType.PDF: 30, + ArtifactType.SPS_PACKAGE: 40, + ArtifactType.VALIDATION_REPORT: 50, + ArtifactType.VALIDATION_EXCEPTIONS: 60, + ArtifactType.MARKED_DOCUMENT: 70, + ArtifactType.INTERMEDIATE_DOCUMENT: 80, + ArtifactType.INPUT: 90, + ArtifactType.SOURCE_DOCUMENT: 100, + ArtifactType.ASSET: 110, + } + + def get_artifact_rows(self, artifacts): + current_xml = self.object.current_artifact(ArtifactType.XML) + current_structure = current_xml.structure if current_xml else None + rows = [] + for artifact in artifacts: + structure = artifact.structure + if not structure and artifact.is_current and artifact.artifact_type in { + ArtifactType.HTML, + ArtifactType.PDF, + ArtifactType.SPS_PACKAGE, + ArtifactType.VALIDATION_REPORT, + ArtifactType.VALIDATION_EXCEPTIONS, + ArtifactType.INTERMEDIATE_DOCUMENT, + }: + structure = current_structure + rows.append( + { + "artifact": artifact, + "filename": PurePosixPath(artifact.file.name).name, + "structure": structure, + "order": self.artifact_order.get(artifact.artifact_type, 999), + } + ) + return rows + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + processings = list(self.object.processings.all()) + events = ( + self.object.events.filter(details__has_key="frontmatter_ai") + .select_related("processing") + .order_by("processing_id", "-created") + ) + latest_ai_by_processing = {} + for event in events: + latest_ai_by_processing.setdefault(event.processing_id, event.details.get("frontmatter_ai")) + context["processings"] = processings + context["processing_rows"] = [ + {"processing": processing, "frontmatter_ai": latest_ai_by_processing.get(processing.pk)} + for processing in processings + ] + artifacts = list(self.object.artifacts.select_related("processing", "structure").order_by("-created")) + artifact_rows = self.get_artifact_rows(artifacts) + context["artifacts"] = artifacts + context["current_artifact_rows"] = sorted( + [row for row in artifact_rows if row["artifact"].is_current], + key=lambda row: (row["order"], row["filename"]), + ) + context["historical_artifact_rows"] = [ + row for row in artifact_rows if not row["artifact"].is_current + ] + context["structure"] = self.object.current_structure + if context["structure"]: + context["references"] = context["structure"].references.select_related( + "reference", "selected_element" + ) + context["citations"] = context["structure"].citations.prefetch_related("references") + context["action_choices"] = ProcessingAction.choices + context["latest_processing"] = self.object.processings.order_by("-created").first() + context["current_xml_artifact"] = self.object.current_artifact(ArtifactType.XML) + context["current_html_artifact"] = self.object.current_artifact(ArtifactType.HTML) + context["current_pdf_artifact"] = self.object.current_artifact(ArtifactType.PDF) + context["current_sps_package_artifact"] = self.object.current_artifact(ArtifactType.SPS_PACKAGE) + context["current_marked_document_artifact"] = self.object.current_artifact(ArtifactType.MARKED_DOCUMENT) + context["current_validation_report_artifact"] = self.object.current_artifact(ArtifactType.VALIDATION_REPORT) + return context diff --git a/manuscripts/wagtail_hooks.py b/manuscripts/wagtail_hooks.py new file mode 100644 index 0000000..716325a --- /dev/null +++ b/manuscripts/wagtail_hooks.py @@ -0,0 +1,175 @@ +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ +from wagtail import hooks +from wagtail.admin.widgets.button import Button +from wagtail.snippets.models import register_snippet +from wagtail.snippets.views.snippets import SnippetViewSet, SnippetViewSetGroup + +from .choices import ArtifactType +from .models.article import Article +from .models.processing import ( + ArticleInput, + Processing, + SPSPackageImport, + XMLImport, +) +from .views.wagtail import ( + ArticleInputCreateView, + ArticleInspectView, + OwnedCreateView, + ProcessingCreateView, + ProcessingInspectView, + SPSPackageImportCreateView, + XMLImportCreateView, +) + + +class ProcessingViewSet(SnippetViewSet): + model = Processing + add_view_class = ProcessingCreateView + inspect_view_class = ProcessingInspectView + inspect_view_enabled = True + inspect_template_name = "manuscripts/processing_inspect.html" + inspect_view_fields = ( + "title", "input_file", "detected_type", "status", + "current_action", "error_message", "created", "updated", + ) + menu_name = "processings" + menu_label = _("Processings") + menu_icon = "tasks" + menu_order = 1 + add_to_admin_menu = False + copy_view_enabled = False + list_display = ("__str__", "detected_type", "status", "current_action", "article_count", "updated") + list_filter = ("status", "detected_type", "current_action") + search_fields = ("title", "input_file", "input_checksum", "error_message") + + +class ArticleViewSet(SnippetViewSet): + model = Article + add_view_class = OwnedCreateView + inspect_view_class = ArticleInspectView + inspect_view_enabled = True + inspect_template_name = "manuscripts/article_inspect.html" + inspect_view_fields = ("title", "doi", "pid", "journal", "issue", "status", "created", "updated") + menu_name = "articles" + menu_label = _("Articles") + menu_icon = "doc-full" + menu_order = 3 + add_to_admin_menu = True + copy_view_enabled = False + list_display = ("title", "doi", "pid", "journal", "status", "updated") + list_filter = ("status", "journal") + search_fields = ("title", "doi", "pid", "content_checksum") + + +class ArticleInputViewSet(SnippetViewSet): + model = ArticleInput + add_view_class = ArticleInputCreateView + menu_label = _("DOCX") + menu_icon = "doc-docx" + add_to_admin_menu = False + list_display = ("__str__", "status", "updated") + + +class XMLImportViewSet(SnippetViewSet): + model = XMLImport + add_view_class = XMLImportCreateView + menu_label = _("XML") + menu_icon = "code" + add_to_admin_menu = False + list_display = ("__str__", "status", "updated") + + +class SPSPackageImportViewSet(SnippetViewSet): + model = SPSPackageImport + add_view_class = SPSPackageImportCreateView + menu_label = _("SPS Package") + menu_icon = "package-zip" + add_to_admin_menu = False + list_display = ("__str__", "status", "updated") + + +class EntryViewSetGroup(SnippetViewSetGroup): + menu_name = "entries" + menu_label = _("Entries") + menu_icon = "upload" + menu_order = 1 + items = (ArticleInputViewSet, XMLImportViewSet, SPSPackageImportViewSet) + + +register_snippet(EntryViewSetGroup) +register_snippet(ArticleViewSet) +register_snippet(ProcessingViewSet) + + +@hooks.register("register_icons") +def register_manuscripts_icons(icons): + return icons + [ + "wagtailadmin/icons/package-zip.svg", + "wagtailadmin/icons/doc-docx.svg", + ] + + +@hooks.register("register_snippet_listing_buttons") +def processing_listing_buttons(snippet, user, next_url=None): + if isinstance(snippet, Processing): + yield Button( + _("Review") if snippet.status == "awaiting_review" else _("View trail"), + reverse( + "manuscripts:processing_review", + args=[snippet.pk], + ) if snippet.status == "awaiting_review" else reverse( + "wagtailsnippets_manuscripts_processing:inspect", args=[snippet.pk] + ), + icon_name="view", + priority=10, + ) + + +@hooks.register("register_snippet_listing_buttons") +def article_listing_buttons(snippet, user, next_url=None): + if not isinstance(snippet, Article): + return + + if snippet.current_structure: + yield Button( + _("View Structure"), + reverse("manuscripts:article_structure_edit", args=[snippet.pk]), + icon_name="code", + priority=50, + attrs={"target": "_blank"}, + ) + + if snippet.current_artifact(ArtifactType.VALIDATION_REPORT): + yield Button( + _("View Validation"), + reverse("manuscripts:article_validation", args=[snippet.pk]), + icon_name="tick-inverse", + priority=40, + attrs={"target": "_blank"}, + ) + + artifact_buttons = [ + (ArtifactType.HTML, _("Preview HTML"), "site", 60, "artifact_preview", {"target": "_blank"}), + (ArtifactType.SPS_PACKAGE, _("Download SPS Package"), "download", 65, "artifact_download", {}), + (ArtifactType.MARKED_DOCUMENT, _("Download Marked DOCX"), "doc-full", 70, "artifact_download", {}), + (ArtifactType.XML, _("Download XML"), "download", 75, "artifact_download", {}), + ] + + for artifact_type, label, icon, priority, url_name, attrs in artifact_buttons: + artifact = snippet.current_artifact(artifact_type) + if artifact: + yield Button( + label, + reverse(f"manuscripts:{url_name}", args=[artifact.pk]), + icon_name=icon, + priority=priority, + attrs=attrs or None, + ) + + +@hooks.register("construct_main_menu") +def simplify_editorial_menu(request, menu_items): + hidden_names = {"snippets", "scielo"} + menu_items[:] = [item for item in menu_items if item.name not in hidden_names] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..b8c192d --- /dev/null +++ b/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +DJANGO_SETTINGS_MODULE = config.settings.test +python_files = tests.py test_*.py *_tests.py +pythonpath = . +addopts = --reuse-db diff --git a/references/__init__.py b/references/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/references/api/__init__.py b/references/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/references/api/v1/__init__.py b/references/api/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/references/api/v1/serializers.py b/references/api/v1/serializers.py new file mode 100644 index 0000000..c897d87 --- /dev/null +++ b/references/api/v1/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from references.models import Reference + + +class ReferenceSerializer(serializers.ModelSerializer): + class Meta: + model = Reference + fields = "__all__" \ No newline at end of file diff --git a/references/api/v1/views.py b/references/api/v1/views.py new file mode 100755 index 0000000..a5b1375 --- /dev/null +++ b/references/api/v1/views.py @@ -0,0 +1,64 @@ +import hashlib +import json + +from django.http import JsonResponse +from rest_framework.mixins import CreateModelMixin +from rest_framework.permissions import IsAuthenticated +from rest_framework.viewsets import GenericViewSet + +from ai.utils.normalizers import stz_norm +from references.api.v1.serializers import ReferenceSerializer +from references.data_utils import get_reference +from references.models import Reference, ReferenceStatus + +# Create your views here. + +class ReferenceViewSet( + GenericViewSet, # generic view functionality + CreateModelMixin, # handles POSTs +): + serializer_class = ReferenceSerializer + permission_classes = [IsAuthenticated] + http_method_names = [ + "post", + ] + + def create(self, request, *args, **kwargs): + # Redirigir a la función api_reference() + return self.api_reference(request) + + def api_reference(self, request): + try: + data = json.loads(request.body) + post_reference = data.get('references') # Obtiene el parámetro + post_type = data.get('type') # Obtiene el parámetro + normalized = stz_norm(post_reference) + checksum = hashlib.sha256(normalized.encode("utf-8")).hexdigest() + + try: + reference = Reference.objects.get(checksum=checksum) + + except Reference.DoesNotExist: + new_reference = Reference.objects.create( + mixed_citation=post_reference, + status=ReferenceStatus.CREATING, + creator=self.request.user, + ) + + get_reference(new_reference.id) + reference = Reference.objects.get(checksum=checksum) + + if post_type == 'xml': + reference_data = reference.element_citation.first().marked_xml + else: + reference_data = reference.element_citation.first().marked + + response_data = { + 'message': f'reference: {reference_data}', + } + except json.JSONDecodeError: + response_data = { + 'error': 'Error processing' + } + + return JsonResponse(response_data) diff --git a/references/apps.py b/references/apps.py new file mode 100755 index 0000000..97a9644 --- /dev/null +++ b/references/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ReferencesConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'references' diff --git a/references/data_utils.py b/references/data_utils.py new file mode 100644 index 0000000..d112440 --- /dev/null +++ b/references/data_utils.py @@ -0,0 +1,147 @@ +import json +import logging +import re + +from celery import shared_task +from lxml import etree + +from ai.references import mark_references +from references.models import ElementCitation, Reference, ReferenceStatus + +logger = logging.getLogger(__name__) + +meses = { + "enero": "01", "febrero": "02", "marzo": "03", "abril": "04", "mayo": "05", "junio": "06", + "julio": "07", "agosto": "08", "septiembre": "09", "octubre": "10", "noviembre": "11", "diciembre": "12", + "january": "01", "february": "02", "march": "03", "april": "04", "may": "05", "june": "06", + "july": "07", "august": "08", "september": "09", "october": "10", "november": "11", "december": "12", + "janeiro": "01", "fevereiro": "02", "março": "03", "abril": "04", "maio": "05", "junho": "06", + "julho": "07", "agosto": "08", "setembro": "09", "outubro": "10", "novembro": "11", "dezembro": "12" +} + +def get_number_of_month(texto): + texto = texto.lower() + for mes, numero in meses.items(): + if re.search(rf"\b{mes}\b", texto): + return numero + return None # Si no encuentra un mes + +def get_xml(json_reference): + try: + json_reference = json.loads(json_reference) + except json.JSONDecodeError as e: + print(f"JSON malformado da IA: {e}") + print(f"JSON recebido: {json_reference[:500]}...") + # Retornar estrutura básica + return etree.Element("error") + root = etree.Element('element-citation', + attrib = { + 'publication-type': json_reference['reftype'], + }) + + if json_reference['reftype'] in ['webpage', 'data']: + NSMAP = {'xlink': 'http://www.w3.org/1999/xlink'} + root = etree.Element('element-citation', + attrib = { + 'publication-type': json_reference['reftype'], + },nsmap=NSMAP) + + if 'authors' in json_reference: + person_group = etree.SubElement(root, "person-group", + attrib = { + 'person-group-type': 'author', + } + ) + for author in json_reference['authors']: + name = etree.Element('name') + if 'fname' in author: + etree.SubElement(name, 'surname').text = author['fname'] + if 'surname' in author: + etree.SubElement(name, 'given-names').text = author['surname'] + if 'collab' in author: + etree.SubElement(name, 'collab').text = author['collab'] + person_group.append(name) + + if json_reference['reftype'] == 'journal': + if 'title' in json_reference: + etree.SubElement(root, 'article-title').text = json_reference['title'] + if 'source' in json_reference: + etree.SubElement(root, 'source').text = json_reference['source'] + if 'vol' in json_reference: + etree.SubElement(root, 'volume').text = str(json_reference['vol']) + if 'num' in json_reference: + etree.SubElement(root, 'issue').text = str(json_reference['num']) + if 'pages' in json_reference and len(str(json_reference['pages']).split('-')) > 0: + etree.SubElement(root, 'fpage').text = str(json_reference['pages']).split('-')[0].strip() + if 'pages' in json_reference and len(str(json_reference['pages']).split('-')) > 1: + etree.SubElement(root, 'lpage').text = str(json_reference['pages']).split('-')[1].strip() + if 'doi' in json_reference: + etree.SubElement(root, 'doi', attrib = { 'pub-id-type': 'doi' }).text = json_reference['doi'] + + if json_reference['reftype'] == 'thesis': + if 'title' in json_reference: + etree.SubElement(root, 'source').text = json_reference['title'] + if 'degree' in json_reference: + etree.SubElement(root, 'comment', attrib = { 'content-type': 'degree' }).text = json_reference['degree'] + if 'organization' in json_reference: + etree.SubElement(root, 'publisher-name').text = json_reference['organization'] + + if json_reference['reftype'] in ['webpage', 'data']: + if 'title' in json_reference: + etree.SubElement(root, 'source').text = json_reference['title'] + if 'uri' in json_reference: + etree.SubElement(root, 'ext-link', attrib = { + 'ext-link-type': 'uri', + '{http://www.w3.org/1999/xlink}href': json_reference['uri'] + }).text = json_reference['uri'] + if 'organization' in json_reference: + etree.SubElement(root, 'publisher-name').text = json_reference['organization'] + if 'country' in json_reference: + etree.SubElement(root, 'publisher-loc').text = json_reference['country'] + if 'doi' in json_reference: + etree.SubElement(root, 'doi', attrib = { 'pub-id-type': 'doi' }).text = json_reference['doi'] + if 'access_date' in json_reference: + match = re.search(r'\b\d{4}\b', json_reference['access_date']) + year = match.group() + month = get_number_of_month(json_reference['access_date']) + etree.SubElement(root, 'date-in-citation', attrib = { + 'content-type': 'access-date', + 'iso-8601-date': year+'-'+month+'-00' + }).text = json_reference['access_date'] + + if 'date' in json_reference: + etree.SubElement(root, 'year').text = str(json_reference['date']) + + + return root + + +@shared_task +def get_reference(obj_id): + logger.info("Iniciando tarefa get_reference para ID=%s", obj_id) + try: + obj_reference = Reference.objects.get(id=obj_id) + logger.info("Executando marcação da citação: %r", obj_reference.mixed_citation) + marked = list(mark_references(obj_reference.mixed_citation)) + + citations_created = 0 + for item in marked: + for i in item['choices']: + try: + marked_data = json.loads(i) if isinstance(i, str) else i + except json.JSONDecodeError: + marked_data = {"raw": i} + citation = ElementCitation.objects.create( + reference=obj_reference, + marked=marked_data, + marked_xml=etree.tostring(get_xml(i), pretty_print=True, encoding='unicode') + ) + citations_created += 1 + logger.debug("Criado ElementCitation ID=%s para a citação marcada: %s", citation.pk, i) + + obj_reference.status = ReferenceStatus.READY + obj_reference.save() + logger.info("Tarefa get_reference concluída com sucesso para ID=%s. Total de citações criadas: %d", obj_id, citations_created) + except Exception as e: + logger.error("Erro na tarefa get_reference para ID=%s: %s", obj_id, e, exc_info=True) + raise diff --git a/references/migrations/0001_initial.py b/references/migrations/0001_initial.py new file mode 100644 index 0000000..9905d30 --- /dev/null +++ b/references/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# Generated by Django 6.0.5 on 2026-06-09 00:35 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='ElementCitation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('sort_order', models.IntegerField(blank=True, editable=False, null=True)), + ('marked', models.JSONField(blank=True, default=dict, verbose_name='Marked')), + ('marked_xml', models.TextField(blank=True, verbose_name='Marked XML')), + ('score', models.IntegerField(blank=True, help_text='Rating from 1 to 10', null=True, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(10)])), + ], + options={ + 'ordering': ['sort_order'], + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Reference', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='Creation date')), + ('updated', models.DateTimeField(auto_now=True, verbose_name='Last update date')), + ('mixed_citation', models.TextField(blank=True, verbose_name='Mixed Citation')), + ('normalized_citation', models.TextField(blank=True, db_index=True, verbose_name='Normalized citation')), + ('checksum', models.CharField(blank=True, max_length=64, unique=True, verbose_name='SHA256')), + ('status', models.IntegerField(blank=True, choices=[(0, 'No reference'), (1, 'Creating reference'), (2, 'Reference ready')], default=0, verbose_name='Reference status')), + ], + options={ + 'verbose_name': 'Reference', + 'verbose_name_plural': 'References', + }, + ), + ] diff --git a/references/migrations/0002_initial.py b/references/migrations/0002_initial.py new file mode 100644 index 0000000..5491417 --- /dev/null +++ b/references/migrations/0002_initial.py @@ -0,0 +1,34 @@ +# Generated by Django 6.0.5 on 2026-06-09 00:35 + +import django.db.models.deletion +import modelcluster.fields +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('references', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='reference', + name='creator', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_creator', to=settings.AUTH_USER_MODEL, verbose_name='Creator'), + ), + migrations.AddField( + model_name='reference', + name='updated_by', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_last_mod_user', to=settings.AUTH_USER_MODEL, verbose_name='Updater'), + ), + migrations.AddField( + model_name='elementcitation', + name='reference', + field=modelcluster.fields.ParentalKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='element_citation', to='references.reference'), + ), + ] diff --git a/references/migrations/__init__.py b/references/migrations/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/references/models.py b/references/models.py new file mode 100755 index 0000000..202f26a --- /dev/null +++ b/references/models.py @@ -0,0 +1,86 @@ +import hashlib + +from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models +from django.utils.translation import gettext_lazy as _ +from modelcluster.fields import ParentalKey +from modelcluster.models import ClusterableModel +from wagtail.admin.panels import FieldPanel, InlinePanel +from wagtail.models import Orderable +from wagtail_json_widget.widgets import JSONEditorWidget + +from ai.utils.normalizers import stz_norm +from core.forms import CoreAdminModelForm +from core.models import CommonControlField + + +class ReferenceStatus(models.IntegerChoices): + NO_REFERENCE = 0, _("No reference") + CREATING = 1, _("Creating reference") + READY = 2, _("Reference ready") + + + +class Reference(CommonControlField, ClusterableModel): + mixed_citation = models.TextField(_("Mixed Citation"), null=False, blank=True) + normalized_citation = models.TextField(_("Normalized citation"), blank=True, db_index=True) + checksum = models.CharField(_("SHA256"), max_length=64, blank=True, unique=True) + + status = models.IntegerField( + _("Reference status"), + choices=ReferenceStatus.choices, + blank=True, + default=ReferenceStatus.NO_REFERENCE + ) + + panels = [ + FieldPanel('mixed_citation'), + InlinePanel('element_citation', label=_("Cited Elements")) + ] + + base_form_class = CoreAdminModelForm + + def __str__(self): + return self.mixed_citation + + def save(self, *args, **kwargs): + self.normalized_citation = stz_norm(self.mixed_citation) + self.checksum = hashlib.sha256(self.normalized_citation.encode("utf-8")).hexdigest() + super().save(*args, **kwargs) + + class Meta: + verbose_name = _("Reference") + verbose_name_plural = _("References") + + +class ElementCitation(Orderable): + reference = ParentalKey( + Reference, on_delete=models.SET_NULL, related_name="element_citation", null=True + ) + marked = models.JSONField(_("Marked"), default=dict, blank=True) + marked_xml = models.TextField(_("Marked XML"), blank=True) + + score = models.IntegerField( + null=True, + blank=True, + validators=[ + MinValueValidator(1), # Mínimo 1 + MaxValueValidator(10) # Máximo 10 + ], + help_text=_("Rating from 1 to 10") + ) + + panels = [ + FieldPanel( + "marked", + widget=JSONEditorWidget( + options={ + "mode": "code", + "modes": ["code", "tree"], + "search": True, + } + ), + ), + FieldPanel("marked_xml"), + FieldPanel("score"), + ] diff --git a/references/wagtail_hooks.py b/references/wagtail_hooks.py new file mode 100644 index 0000000..59902f2 --- /dev/null +++ b/references/wagtail_hooks.py @@ -0,0 +1,57 @@ +# Third-party imports +import hashlib + +from django.http import HttpResponseRedirect +from django.utils.translation import gettext_lazy as _ +from wagtail.snippets.models import register_snippet +from wagtail.snippets.views.snippets import CreateView, SnippetViewSet + +# Local application imports +from ai.utils.normalizers import stz_norm +from config.menu import get_menu_order +from references.data_utils import get_reference +from references.models import Reference + + +class ReferenceCreateView(CreateView): + def form_valid(self, form): + # Obtener el contenido de mixed_citation del formulario + mixed_citation_text = form.cleaned_data["mixed_citation"].strip() + lineas = mixed_citation_text.split("\n") # Dividir por saltos de línea + + # Crear un nuevo objeto Reference por cada línea válida + for linea in lineas: + linea = linea.strip() # Eliminar espacios adicionales en cada línea + if linea: # Evitar procesar líneas vacías + normalized = stz_norm(linea) + checksum = hashlib.sha256(normalized.encode("utf-8")).hexdigest() + new_reference, created = Reference.objects.get_or_create( + checksum=checksum, + defaults={ + "mixed_citation": linea, + "normalized_citation": normalized, + "status": 1, + "creator": self.request.user, + }, + ) + if created: + get_reference.delay(new_reference.id) + print(f"Creado Reference: {new_reference.mixed_citation}") + + # Redirigir después de la creación de los objetos + return HttpResponseRedirect(self.get_success_url()) + + +class ReferenceModelViewSet(SnippetViewSet): + model = Reference + add_view_class = ReferenceCreateView + menu_name = "references" + menu_label = _("References") + menu_icon = "openquote" + menu_order = get_menu_order("references") + exclude_from_explorer = False + list_per_page = 20 + add_to_admin_menu = True + + +register_snippet(ReferenceModelViewSet) diff --git a/requirements/base.txt b/requirements/base.txt new file mode 100644 index 0000000..3ceeba1 --- /dev/null +++ b/requirements/base.txt @@ -0,0 +1,60 @@ +setuptools>=68.2.2,<82 +whitenoise==6.12.0 # https://github.com/evansd/whitenoise +redis==7.4.0 # https://github.com/redis/redis-py +celery==5.3.6 # pyup: < 6.0 # https://github.com/celery/celery +flower==2.0.1 # https://github.com/mher/flower +hiredis==2.2.3 # https://github.com/redis/hiredis-py + +# Django +# ------------------------------------------------------------------------------ +django==6.0.5 +django-environ==0.13.0 +djangorestframework==3.17.1 +djangorestframework-simplejwt==5.5.1 # https://django-rest-framework-simplejwt.readthedocs.io/en/latest/ +django-celery-beat==2.9.0 # https://github.com/celery/django-celery-beat +django_celery_results==2.6.0 +django-compressor==4.6.0 # https://github.com/django-compressor/django-compressor + +# Wagtail +# ------------------------------------------------------------------------------ +wagtail==7.4 +wagtail-modeladmin==2.3.0 +wagtail-autocomplete==0.12.0 +wagtail-json-widget + +# lxml +# ------------------------------------------------------------------------------ +lxml==4.9.4 # https://github.com/lxml/lxml + +# Kombu +# ------------------------------------------------------------------------------ +kombu==5.6.2 + +# Tenacity +# ------------------------------------------------------------------------------ +tenacity==8.2.3 # https://pypi.org/project/tenacity/ + +# Packtools +# ------------------------------------------------------------------------------ +git+https://git@github.com/scieloorg/packtools@4.12.6#egg=packtools + +# Langdetect +# ------------------------------------------------------------------------------ +langdetect~=1.0.9 +langid + +# Google Generative AI +# ------------------------------------------------------------------------------ +google-generativeai==0.3.2 + +# Docx +# ------------------------------------------------------------------------------ +python-docx==1.1.2 + +# Docling (text extraction from DOCX with OCR) +# ------------------------------------------------------------------------------ +docling + +# huggingface-hub +# ------------------------------------------------------------------------------ +huggingface_hub==0.26.1 # https://pypi.org/project/huggingface-hub/ diff --git a/requirements/extra-llama.txt b/requirements/extra-llama.txt new file mode 100644 index 0000000..c949747 --- /dev/null +++ b/requirements/extra-llama.txt @@ -0,0 +1,4 @@ +# Llama +# ------------------------------------------------------------------------------ +huggingface-hub==0.26.1 +llama-cpp-python==0.3.14 diff --git a/requirements/local.txt b/requirements/local.txt new file mode 100644 index 0000000..8cd124d --- /dev/null +++ b/requirements/local.txt @@ -0,0 +1,15 @@ +-r base.txt + +Werkzeug==3.0.1 # https://github.com/pallets/werkzeug +ipdb==0.13.13 # https://github.com/gotcha/ipdb +psycopg2-binary==2.9.9 # https://github.com/psycopg/psycopg2 +watchgod==0.8.2 # https://github.com/samuelcolvin/watchgod + +django-extensions==3.2.3 # https://github.com/django-extensions/django-extensions +django-debug-toolbar # https://github.com/jazzband/django-debug-toolbar + +pytest==9.0.3 +pytest-django==4.11.1 +pytest-cov==7.1.0 +coverage==7.10.6 +django-coverage-plugin==3.1.0 \ No newline at end of file diff --git a/requirements/production.txt b/requirements/production.txt new file mode 100644 index 0000000..a4042c1 --- /dev/null +++ b/requirements/production.txt @@ -0,0 +1,18 @@ +# PRECAUTION: avoid production dependencies that aren't in development + +-r base.txt + +gevent==26.4.0 # http://www.gevent.org/ +gunicorn==26.0.0 +psycopg2-binary==2.9.9 # https://github.com/psycopg/psycopg2 +sentry-sdk[django]==2.60.0 + +# Django +# ------------------------------------------------------------------------------ +django-anymail # https://github.com/anymail/django-anymail +setuptools>=68.2.2,<82 + + +# Elastic-APM # https://pypi.org/project/elastic-apm/ +# ------------------------------------------------------------------------------ +elastic-apm==6.21.4.post8347027212 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..d435e47 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,58 @@ +[flake8] +max-line-length = 120 +exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv + +[pycodestyle] +max-line-length = 120 +exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv + +[isort] +line_length = 88 +known_first_party = core,config +multi_line_output = 3 +default_section = THIRDPARTY +skip = venv/ +skip_glob = **/migrations/*.py +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true + +[mypy] +python_version = 3.12 +check_untyped_defs = True +ignore_missing_imports = True +warn_unused_ignores = True +warn_redundant_casts = True +warn_unused_configs = True +plugins = mypy_django_plugin.main + +[mypy.plugins.django-stubs] +django_settings_module = config.settings.test + +[mypy-*.migrations.*] +# Django migrations should not produce any errors: +ignore_errors = True + +[coverage:run] +include = + ai/* + core/* + core_settings/* + journals/* + labeling/* + manuscripts/* + references/* + sps/* + users/* +omit = + *migrations* + *tests* + */tests/* + */templates/* +plugins = + django_coverage_plugin + +[coverage:report] +fail_under = 100 +include = + manuscripts/* diff --git a/sps/__init__.py b/sps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sps/apps.py b/sps/apps.py new file mode 100644 index 0000000..2920b9d --- /dev/null +++ b/sps/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class SPSConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "sps" + verbose_name = "SPS" diff --git a/sps/exceptions.py b/sps/exceptions.py new file mode 100644 index 0000000..f18df97 --- /dev/null +++ b/sps/exceptions.py @@ -0,0 +1,22 @@ +class XMLFileParsingError(Exception): + pass + + +class XMLFileValidationError(Exception): + pass + + +class XMLFileDocxGenerationError(Exception): + pass + + +class XMLFilePDFGenerationError(Exception): + pass + + +class XMLFileHTMLGenerationError(Exception): + pass + + +class SPSPackageValidationError(Exception): + pass diff --git a/sps/models.py b/sps/models.py new file mode 100644 index 0000000..32d618f --- /dev/null +++ b/sps/models.py @@ -0,0 +1 @@ +"""Technical XML services do not own persisted operational models.""" diff --git a/sps/utils.py b/sps/utils.py new file mode 100644 index 0000000..1d2b54d --- /dev/null +++ b/sps/utils.py @@ -0,0 +1,372 @@ +import copy +import html as html_lib +import os +import re + +from django.conf import settings +from lxml import etree +from packtools import HTMLGenerator, data_checker +from packtools.sps.formats.pdf.pipeline import docx +from packtools.sps.formats.pdf.pipeline.xml import extract_article_main_language +from packtools.sps.models.article_license import ArticleLicense +from packtools.sps.pid_provider.models.journal_meta import JournalID, Publisher, Title +from packtools.sps.pid_provider.xml_sps_lib import XMLWithPre +from packtools.sps.validation.xml_validator import get_validation_results + +from sps import exceptions +from sps.xref import apply_xml_xrefs_to_docx + +_BIBR_REF_ANCHOR_RE = re.compile(r'(<a\b(?=[^>]*\bname="(B\d+_ref)")(?!(?=[^>]*\bid=))[^>]*)(>)') +_BIBR_REF_HREF_RE = re.compile(r'href="#(B\d+)(?:(?:%20|\s+)B\d+)+_ref"') +_HTML_ASSET_ATTR_RE = re.compile(r'(?P<attr>\b(?:src|href))="(?P<value>[^"#?:]+)"') + + +_BIBR_LINK_STYLE = """ +<style> +.xref a[href^="#"] { + cursor: pointer; + pointer-events: auto; +} +li:target, +[id$="_ref"]:target { + scroll-margin-top: 1.5rem; +} +</style> +""" + +_BIBR_LINK_SCRIPT = """ +<script> +document.addEventListener("click", function (event) { + var link = event.target.closest && event.target.closest(".xref a[href^='#']"); + if (!link) { + return; + } + var targetId = decodeURIComponent(link.getAttribute("href").slice(1)); + var namedTargets = document.getElementsByName(targetId); + var target = document.getElementById(targetId) || (namedTargets.length ? namedTargets[0] : null); + if (!target) { + return; + } + event.preventDefault(); + var scrollTarget = target.closest("li") || target; + scrollTarget.scrollIntoView({ behavior: "smooth", block: "start" }); + if (history.pushState) { + history.pushState(null, "", "#" + targetId); + } else { + location.hash = targetId; + } +}, true); +</script> +""" + + +def _normalize_html_bibr_links(html: str) -> str: + def add_id(match): + return f'{match.group(1)} id="{match.group(2)}"{match.group(3)}' + + html = _BIBR_REF_ANCHOR_RE.sub(add_id, html) + return _BIBR_REF_HREF_RE.sub(lambda match: f'href="#{match.group(1)}_ref"', html) + + +def _normalize_html_asset_links(html: str, asset_url_map: dict | None = None) -> str: + if not asset_url_map: + return html + + def replace(match): + value = match.group("value") + asset_url = asset_url_map.get(os.path.basename(value)) + if not asset_url: + return match.group(0) + return f'{match.group("attr")}="{html_lib.escape(asset_url, quote=True)}"' + + return _HTML_ASSET_ATTR_RE.sub(replace, html) + + +def validate_xml_document(xml_file_path, output_root_dir, params): + if not os.path.exists(output_root_dir): + os.makedirs(output_root_dir) + + base_fname, fext = os.path.splitext(os.path.basename(xml_file_path)) + path_csv = os.path.join(output_root_dir, f"{base_fname}.validation.csv") + path_exceptions = os.path.join(output_root_dir, f"{base_fname}.exceptions.json") + + try: + validator = data_checker.XMLDataChecker( + path_csv, path_exceptions, xml_file_path + ) + validator.validate(params=params, csv_per_xml=False) + except Exception as e: + raise exceptions.XMLFileValidationError(f"Error during XML validation: {e}") + + return path_csv, path_exceptions + + +def _extract_journal_data(xmltree): + try: + license_code = None + for lic in ArticleLicense(xmltree).licenses: + code = lic.get("code") + if code: + license_code = code + break + return { + "abbrev_journal_title": Title(xmltree).abbreviated_journal_title, + "publisher_name_list": Publisher(xmltree).publishers_names, + "nlm_journal_title": JournalID(xmltree).nlm_ta, + "license_code": license_code, + } + except Exception: + return {} + + +def validate_zip(zip_path: str) -> tuple[list, list]: + rows = [] + exceptions = [] + for xml_with_pre in XMLWithPre.create(path=zip_path): + xmltree = xml_with_pre.xmltree + rules = {"journal_data": _extract_journal_data(xmltree)} + for result in get_validation_results(xmltree, rules): + if not result: + continue + if result.get("response") == "exception": + exceptions.append(result) + continue + if result.get("response") == "OK": + continue + group = result.get("group", "") + item = result.get("item") or "" + sub_item = result.get("sub_item") or "" + attribute = "/".join(filter(None, [item, sub_item])) + rows.append( + { + "group": group, + "title": result.get("title"), + "parent": result.get("parent"), + "parent_id": result.get("parent_id"), + "parent_article_type": result.get("parent_article_type"), + "item": item, + "sub_item": sub_item, + "attribute": attribute, + "validation_type": result.get("validation_type"), + "response": result.get("response"), + "expected_value": result.get("expected_value"), + "got_value": result.get("got_value"), + "advice": result.get("advice"), + } + ) + return rows, exceptions + + +def prepare_xml_for_pdf(xml_tree): + """ + Return a rendering-only XML copy with the values required by packtools PDF. + + Manuscripts can legitimately reach SciELO Tools before journal, DOI, issue, and + other publication metadata are assigned. The packtools PDF pipeline still + expects some of those nodes to exist, so provide neutral display fallbacks + without changing the canonical SPS XML. + """ + tree = copy.deepcopy(xml_tree) + + front = tree.find("front") + if front is None: + front = etree.Element("front") + tree.insert(0, front) + + journal_meta = front.find("journal-meta") + if journal_meta is None: + journal_meta = etree.Element("journal-meta") + front.insert(0, journal_meta) + + journal_title_group = journal_meta.find("journal-title-group") + if journal_title_group is None: + journal_title_group = etree.SubElement(journal_meta, "journal-title-group") + if journal_title_group.find("journal-title") is None: + etree.SubElement(journal_title_group, "journal-title").text = ( + "Journal not assigned" + ) + + article_meta = front.find("article-meta") + if article_meta is None: + article_meta = etree.SubElement(front, "article-meta") + + if article_meta.find('.//article-id[@pub-id-type="doi"]') is None: + etree.SubElement(article_meta, "article-id", {"pub-id-type": "doi"}).text = ( + "pending" + ) + + subject = article_meta.find( + './/subj-group[@subj-group-type="heading"]/subject' + ) + if subject is None: + article_categories = article_meta.find("article-categories") + if article_categories is None: + article_categories = etree.SubElement(article_meta, "article-categories") + subj_group = etree.SubElement( + article_categories, "subj-group", {"subj-group-type": "heading"} + ) + etree.SubElement(subj_group, "subject").text = ( + tree.get("article-type") or "article" + ) + + if article_meta.find(".//article-title") is None: + title_group = article_meta.find("title-group") + if title_group is None: + title_group = etree.SubElement(article_meta, "title-group") + etree.SubElement(title_group, "article-title").text = "Untitled article" + + if tree.find(".//fn-group") is None: + back = tree.find("back") + if back is None: + back = etree.SubElement(tree, "back") + fn_group = etree.SubElement(back, "fn-group") + fn = etree.SubElement(fn_group, "fn", {"fn-type": "other"}) + etree.SubElement(fn, "p").text = "" + + return tree + + +def generate_pdf_for_xml_document(xml_file_path, output_root_dir, params): + if not os.path.exists(output_root_dir): + os.makedirs(output_root_dir) + + base_name = os.path.basename(xml_file_path) + f_name, f_ext = os.path.splitext(base_name) + path_pdf = os.path.join(output_root_dir, f"{f_name}.pdf") + path_docx = os.path.join(output_root_dir, f"{f_name}.docx") + + try: + xml_root = etree.parse(xml_file_path).getroot() + language = extract_article_main_language(xml_root) + rendered_xml = prepare_xml_for_pdf(xml_root) + + data = { + "base_layout": os.path.join( + settings.BASE_DIR, "docx_parser", "layouts", "two_cols.docx" + ), + } + assets_dir = params.get("assets_dir") + if assets_dir: + data["assets_dir"] = assets_dir + + doc = docx.pipeline_docx(rendered_xml, data) + doc = apply_xml_xrefs_to_docx(doc, rendered_xml) + doc.save(path_docx) + + _convert_docx_to_pdf(path_docx, path_pdf) + except Exception as e: + raise exceptions.XMLFilePDFGenerationError( + f"Error generating PDF: {e}" + ) + + return path_pdf, path_docx, language + + +def _convert_docx_to_pdf(docx_path, pdf_path): + import subprocess + outdir = os.path.dirname(pdf_path) + subprocess.run( + [ + "soffice", + "--headless", + "--convert-to", + "pdf", + "--outdir", + outdir, + docx_path, + ], + capture_output=True, + timeout=120, + check=True, + ) + generated = os.path.join( + outdir, + os.path.splitext(os.path.basename(docx_path))[0] + ".pdf", + ) + if os.path.isfile(generated) and generated != pdf_path: + os.rename(generated, pdf_path) + if not os.path.isfile(pdf_path): + raise RuntimeError(f"PDF not generated at {pdf_path}") + + +def generate_html_for_xml_document(xml_file_path, output_root_dir, config, asset_url_map=None): + if not os.path.exists(output_root_dir): + os.makedirs(output_root_dir) + + config = config or {} + try: + generator = HTMLGenerator.parse( + xml_file_path, + valid_only=config.get("valid_only", False), + xslt=config.get("xslt", "3.0"), + output_style=config.get("output_style", "website"), + css=config.get("css", ""), + print_css=config.get("print_css", ""), + js=config.get("js", ""), + bootstrap_css=config.get("bootstrap_css", ""), + article_css=config.get("article_css", ""), + math_elem_preference=config.get("math_elem_preference", "mml:math"), + math_js=config.get("math_js", ""), + permlink=config.get("permlink", ""), + url_article_page=config.get("url_article_page", ""), + url_download_ris=config.get("url_download_ris", ""), + gs_abstract=config.get("gs_abstract", False), + design_system_static_img_path=config.get( + "design_system_static_img_path", "" + ), + ) + except Exception as e: + raise exceptions.XMLFileParsingError(f"Error parsing XML file: {e}") + + base_name = os.path.splitext(os.path.basename(xml_file_path))[0] + try: + lang, result = next(iter(generator)) + except StopIteration as e: + raise exceptions.XMLFileHTMLGenerationError( + "The XML does not define a document language." + ) from e + except Exception as e: + raise exceptions.XMLFileHTMLGenerationError( + f"Error converting XML to HTML: {e}" + ) from e + + path_html = os.path.join(output_root_dir, f"{base_name}-{lang}.html") + body = etree.tostring( + result, + pretty_print=True, + encoding="unicode", + method="html", + ) + + stylesheets = [] + for key in ("bootstrap_css", "article_css", "css", "print_css"): + href = config.get(key) + if href and href not in stylesheets: + stylesheets.append(href) + stylesheet_tags = "".join( + f'<link rel="stylesheet" href="{html_lib.escape(href, quote=True)}">' + for href in stylesheets + ) + + scripts = [] + for key in ("js", "math_js"): + src = config.get(key) + if src and src not in scripts: + scripts.append(src) + script_tags = "".join( + f'<script src="{html_lib.escape(src, quote=True)}"></script>' for src in scripts + ) + + html = ( + "<!DOCTYPE html>\n" + f'<html lang="{html_lib.escape(lang, quote=True)}">' + '<head><meta charset="utf-8">' + f"<title>{html_lib.escape(base_name)}{stylesheet_tags}{_BIBR_LINK_STYLE}" + f"{body}{script_tags}{_BIBR_LINK_SCRIPT}" + ) + html = _normalize_html_bibr_links(html) + html = _normalize_html_asset_links(html, asset_url_map) + with open(path_html, "w", encoding="utf-8") as fp: + fp.write(html) + + return path_html, lang diff --git a/sps/xml.py b/sps/xml.py new file mode 100644 index 0000000..e04e35f --- /dev/null +++ b/sps/xml.py @@ -0,0 +1,1251 @@ +import html +import os +import re +from datetime import date + +from django.utils.dateparse import parse_date +from lxml import etree +from wagtail.images import get_image_model + +from labeling.fragments import ( + append_fragment, + extract_subsection, + iter_front_blocks, + normalize_aff_ids, + parse_xml_fragment, + process_special_content, + sanitize_inline_xml_fragment, +) +from labeling.segmentation import process_labeled_text +from sps.xref import make_text_xref_fn_from_refs + +_XREF_SPLIT_RE = re.compile(r'(]*>.*?)', re.DOTALL) +MONTHS = { + "january": 1, + "jan": 1, + "enero": 1, + "janeiro": 1, + "february": 2, + "feb": 2, + "febrero": 2, + "fevereiro": 2, + "march": 3, + "mar": 3, + "marzo": 3, + "marco": 3, + "março": 3, + "april": 4, + "apr": 4, + "abril": 4, + "may": 5, + "mayo": 5, + "maio": 5, + "june": 6, + "jun": 6, + "junio": 6, + "junho": 6, + "july": 7, + "jul": 7, + "julio": 7, + "julho": 7, + "august": 8, + "aug": 8, + "agosto": 8, + "september": 9, + "sep": 9, + "sept": 9, + "septiembre": 9, + "setembro": 9, + "october": 10, + "oct": 10, + "octubre": 10, + "outubro": 10, + "november": 11, + "nov": 11, + "noviembre": 11, + "novembro": 11, + "december": 12, + "dec": 12, + "diciembre": 12, + "dezembro": 12, +} + + +def _apply_to_segments(text, fn): + """Apply fn only to plain-text segments, leaving existing tags intact.""" + parts = _XREF_SPLIT_RE.split(text) + return ''.join(fn(part) if i % 2 == 0 else part for i, part in enumerate(parts)) + + +def _apply_xref_map(paragraph, xref_map): + """Apply xref_map replacements one citation at a time. + + Each citation is applied via a fresh _apply_to_segments pass so that + tags created by earlier iterations are respected as boundaries + for later, shorter keys (e.g. "20" is replaced first, then "2" must + not touch the already-created rid="B20" attribute). + """ + for cit_text, rid in sorted(xref_map.items(), key=lambda x: -len(x[0])): + replacement = f'{cit_text}' + paragraph = _apply_to_segments( + paragraph, lambda seg, ct=cit_text, r=replacement: seg.replace(ct, r) + ) + return paragraph + + +def _append_formula_fragment(node_dest, value): + if not value: + return + fragment = value.strip() + if fragment.startswith("<"): + fragment = html.unescape(fragment) + fragment = fragment.replace( + 'xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"', + "", + ) + try: + wrapper = etree.fromstring(f"{fragment}".encode("utf-8")) + except etree.XMLSyntaxError: + parser = etree.XMLParser(recover=True) + wrapper = etree.fromstring( + f"{fragment}".encode("utf-8"), parser=parser + ) + if not len(wrapper): + append_fragment(node_dest, value) + return + + if wrapper.text: + node_dest.text = (node_dest.text or "") + wrapper.text + for child in list(wrapper): + node_dest.append(child) + + +def _append_node_contents(node_dest, node_src): + if node_src.text: + if len(node_dest): + node_dest[-1].tail = (node_dest[-1].tail or "") + node_src.text + else: + node_dest.text = (node_dest.text or "") + node_src.text + for child in list(node_src): + node_dest.append(child) + + +def _append_inline_text_fragment(node_dest, value): + node_tmp = etree.Element("_tmp") + append_fragment(node_tmp, value) + _append_node_contents(node_dest, node_tmp) + + +def _is_abstract_paragraph(block): + return block.block_type in {"paragraph", "paragraph_with_language"} and block.value.get( + "label" + ) == "" + + +def _is_abstract_continuation(block): + if block.block_type == "paragraph" and block.value.get("label") in { + "

    ", + "", + }: + return True + return _is_abstract_paragraph(block) + + +def _apply_process_labeled_text(paragraph, data_back): + """Apply process_labeled_text to each plain-text segment independently.""" + def process_segment(seg): + if not seg: + return seg + refs = process_labeled_text(seg, data_back) + for r in refs: + if r.get('refid') and not re.search( + rf']*>{re.escape(r["cita"])}', seg + ): + seg = seg.replace( + r['cita'], + f'{r["cita"]}', + ) + return seg + return _apply_to_segments(paragraph, process_segment) + + +def extract_date(texto): + try: + parsed = parse_date(texto or "") + if parsed: + return (parsed.strftime("%d"), parsed.strftime("%m"), parsed.strftime("%Y")) + + # Patrón para detectar YYYY-MM-DD, YYYY/MM/DD, DD-MM-YYYY, DD/MM/YYYY + patron_fecha = ( + r"\b(\d{4})[-/](\d{1,2})[-/](\d{1,2})\b|\b(\d{1,2})[-/](\d{1,2})[-/](\d{4})\b" + ) + + match = re.search(patron_fecha, texto or "") + if match: + if match.group(1): # Formato YYYY-MM-DD o YYYY/MM/DD + año = match.group(1) + mes = match.group(2).zfill(2) + dia = match.group(3).zfill(2) + else: # Formato DD-MM-YYYY o DD/MM/YYYY + dia = match.group(4).zfill(2) + mes = match.group(5).zfill(2) + año = match.group(6) + return (dia, mes, año) + + normalized = _strip_accents((texto or "").lower()) + month_names = "|".join(sorted(MONTHS, key=len, reverse=True)) + patterns = [ + rf"\b({month_names})\.?\s+(\d{{1,2}}),?\s+(\d{{4}})\b", + rf"\b(\d{{1,2}})\s+(?:de\s+)?({month_names})\.?,?\s+(?:de\s+)?(\d{{4}})\b", + ] + for pattern in patterns: + match = re.search(pattern, normalized) + if not match: + continue + if match.group(1).isdigit(): + dia, mes, año = match.group(1), MONTHS[match.group(2)], match.group(3) + else: + mes, dia, año = MONTHS[match.group(1)], match.group(2), match.group(3) + parsed = date(int(año), int(mes), int(dia)) + return (parsed.strftime("%d"), parsed.strftime("%m"), parsed.strftime("%Y")) + except Exception: + pass + + return None # No se encontró + + +def _strip_accents(text): + return ( + text.replace("á", "a") + .replace("é", "e") + .replace("í", "i") + .replace("ó", "o") + .replace("ú", "u") + .replace("ã", "a") + .replace("õ", "o") + .replace("ç", "c") + .replace("ñ", "n") + ) + + +def _wrap_table_rows(table_element): + """Wrap

    rows inside
    with proper / elements. + + The packtools PDF pipeline (extract_table_data) looks for .//thead and + .//tbody to extract header and data rows. HTML tables stored in the + structure have directly under
    , so this function normalises + the tree in-place before the element is appended to the XML tree. + + - Rows where every cell is . + - All remaining rows are grouped into . + - Already-wrapped tables (existing thead/tbody) are left untouched. + """ + for table in table_element.iter("table"): + # Skip tables that already have thead or tbody + if table.find("thead") is not None or table.find("tbody") is not None: + continue + + direct_rows = [child for child in list(table) if child.tag == "tr"] + if not direct_rows: + continue + + # Remove the bare elements from the table first + for tr in direct_rows: + table.remove(tr) + + header_rows = [] + data_rows = [] + header_done = False + for tr in direct_rows: + cells = list(tr) + if not header_done and cells and all(c.tag == "th" for c in cells): + header_rows.append(tr) + else: + header_done = True + data_rows.append(tr) + + insert_index = 0 + if header_rows: + thead = etree.SubElement(table, "thead") + for tr in header_rows: + thead.append(tr) + # Move thead to front + table.remove(thead) + table.insert(insert_index, thead) + insert_index += 1 + + if data_rows: + tbody = etree.SubElement(table, "tbody") + for tr in data_rows: + tbody.append(tr) + table.remove(tbody) + table.insert(insert_index, tbody) + + +def get_xml(article_docx, data_front, data, data_back, xref_map=None): + # Build narrative Author (year) xref replacer from data_back reference texts + _text_xref_refs = [ + { + 'rid': item['value'].get('refid') or f'B{i + 1}', + 'ref_text': item['value'].get('paragraph') or '', + } + for i, item in enumerate(data_back) + if item.get('value') + ] + _text_xref_fn = make_text_xref_fn_from_refs(_text_xref_refs) + + # Crear el elemento raíz + nsmap = { + "mml": "http://www.w3.org/1998/Math/MathML", + "xlink": "http://www.w3.org/1999/xlink", + } + root = etree.Element( + "article", + nsmap=nsmap, + attrib={ + "article-type": "research-article", + "dtd-version": "1.1", + "specific-use": "sps-1.9", + "{http://www.w3.org/XML/1998/namespace}lang": article_docx.language or "en", + } + #'{http://www.w3.org/1998/Math/MathML}mml': 'http://www.w3.org/1998/Math/MathML', + #'{http://www.w3.org/1999/xlink}xlink': 'http://www.w3.org/1999/xlink'} + ) + + # Añadir un elemento hijo + front = etree.SubElement(root, "front") + body = etree.SubElement(root, "body") + back = etree.SubElement(root, "back") + node_reflist = etree.SubElement(back, "ref-list") + + subsec = None + continue_t = False + arr_subarticle = [] + + node = etree.SubElement(front, "journal-meta") + + if article_docx.acronym: + node_tmp = etree.SubElement(node, "journal-id") + node_tmp.set("journal-id-type", "publisher-id") + node_tmp.text = article_docx.acronym + + if article_docx.title_nlm: + node_tmp = etree.SubElement(node, "journal-id") + node_tmp.set("journal-id-type", "nlm-ta") + node_tmp.text = article_docx.title_nlm + + node_tmp = etree.SubElement(node, "journal-title-group") + + if article_docx.journal_title: + node_tmp2 = etree.SubElement(node_tmp, "journal-title") + node_tmp2.text = article_docx.journal_title + + if article_docx.short_title: + node_tmp2 = etree.SubElement(node_tmp, "abbrev-journal-title") + node_tmp2.set("abbrev-type", "publisher") + node_tmp2.text = article_docx.short_title + + if article_docx.pissn: + node_tmp = etree.SubElement(node, "issn") + node_tmp.set("pub-type", "ppub") + node_tmp.text = article_docx.pissn + + if article_docx.eissn: + node_tmp = etree.SubElement(node, "issn") + node_tmp.set("pub-type", "epub") + node_tmp.text = article_docx.eissn + + node_tmp = etree.SubElement(node, "publisher") + + if article_docx.pubname: + node_tmp2 = etree.SubElement(node_tmp, "publisher-name") + node_tmp2.text = article_docx.pubname + + ##### Article Meta + + translates = [] + current_trans = [] + + for block in iter_front_blocks(article_docx, data_front): + if ( + block.block_type == "paragraph_with_language" + and block.value.get("label") == "" + ): + # Si ya tenemos contenido acumulado, lo guardamos como parte + if current_trans: + translates.append(current_trans) + current_trans = [] + current_trans.append(block) + + if current_trans: + translates.append(current_trans) + + for i, data_t in enumerate(translates): + if i == 0: + node = etree.SubElement(front, "article-meta") + else: + subarticle = etree.SubElement(root, "sub-article") + arr_subarticle.append(subarticle) + subarticle.attrib["article-type"] = "translation" + subarticle.attrib["id"] = f"S{len(arr_subarticle)}" + subarticle.attrib["{http://www.w3.org/XML/1998/namespace}lang"] = data_t[ + 0 + ].value["language"] + + node = etree.SubElement(subarticle, "front-stub") + + val = next( + ( + b.value["paragraph"] + for b in data_t + if b.block_type == "paragraph" + and b.value.get("label") == "" + ), + None, + ) + + if val: + node_tmp = etree.SubElement(node, "article-id") + node_tmp.set("pub-id-type", "doi") + node_tmp.text = val + + val = next( + ( + b.value["paragraph"] + for b in data_t + if b.block_type == "paragraph" and b.value.get("label") == "" + ), + None, + ) + + if val: + node_tmp = etree.SubElement(node, "article-categories") + node_tmp2 = etree.SubElement(node_tmp, "subj-group") + node_tmp2.set("subj-group-type", "heading") + node_tmp3 = etree.SubElement(node_tmp2, "subject") + node_tmp3.text = val + + val = next( + ( + b.value["paragraph"] + for b in data_t + if b.block_type == "paragraph_with_language" + and b.value.get("label") == "" + ), + None, + ) + + if val: + node_tmp = etree.SubElement(node, "title-group") + node_tmp2 = etree.SubElement(node_tmp, "article-title") + append_fragment(node_tmp2, val) + + vals = [ + b + for b in data_t + if b.block_type == "paragraph_with_language" + and b.value.get("label") == "" + ] + + for val in vals: + node_tmp2 = etree.SubElement(node_tmp, "trans-title-group") + node_tmp2.set( + "{http://www.w3.org/XML/1998/namespace}lang", + val.value.get("language"), + ) + node_tmp3 = etree.SubElement(node_tmp2, "trans-title") + append_fragment(node_tmp3, val.value.get("paragraph")) + + node_tmp = etree.SubElement(node, "contrib-group") + + vals = [b for b in data_t if b.block_type == "author_paragraph"] + + for val in vals: + node_tmp2 = etree.SubElement(node_tmp, "contrib") + node_tmp2.set("contrib-type", "author") + if val.value.get("orcid"): + node_tmp3 = etree.SubElement(node_tmp2, "contrib-id") + node_tmp3.set("contrib-id-type", "orcid") + node_tmp3.text = val.value.get("orcid") + node_tmp3 = etree.SubElement(node_tmp2, "name") + if val.value.get("surname"): + node_tmp4 = etree.SubElement(node_tmp3, "surname") + append_fragment(node_tmp4, val.value.get("surname")) + + if val.value.get("given_names"): + node_tmp4 = etree.SubElement(node_tmp3, "given-names") + append_fragment(node_tmp4, val.value.get("given_names")) + + for aff_id in normalize_aff_ids(val.value.get("affid")): + node_tmp3 = etree.SubElement(node_tmp2, "xref") + node_tmp3.set("ref-type", "aff") + node_tmp3.set("rid", f"aff{aff_id}") + node_tmp3.text = val.value.get("char") or ("*" * int(aff_id)) + + vals = [b for b in data_t if b.block_type == "aff_paragraph"] + + for val in vals: + aff_ids = normalize_aff_ids(val.value.get("affid")) + if not aff_ids: + continue + aff_id = aff_ids[0] + node_tmp = etree.SubElement(node, "aff") + node_tmp.set("id", f"aff{aff_id}") + + node_tmp2 = etree.SubElement(node_tmp, "label") + node_tmp2.text = val.value.get("char") or ("*" * int(aff_id)) + + if val.value.get("orgname"): + node_tmp2 = etree.SubElement(node_tmp, "institution") + node_tmp2.set("content-type", "orgname") + append_fragment(node_tmp2, val.value.get("orgname")) + + if val.value.get("orgdiv1"): + node_tmp2 = etree.SubElement(node_tmp, "institution") + node_tmp2.set("content-type", "orgdiv1") + append_fragment(node_tmp2, val.value.get("orgdiv1")) + + if val.value.get("orgdiv2"): + node_tmp2 = etree.SubElement(node_tmp, "institution") + node_tmp2.set("content-type", "orgdiv2") + append_fragment(node_tmp2, val.value.get("orgdiv2")) + + node_tmp2 = etree.SubElement(node_tmp, "addr-line") + + if val.value.get("city"): + node_tmp3 = etree.SubElement(node_tmp2, "city") + append_fragment(node_tmp3, val.value.get("city")) + + if val.value.get("state"): + node_tmp3 = etree.SubElement(node_tmp2, "state") + append_fragment(node_tmp3, val.value.get("state")) + + if val.value.get("country"): + node_tmp2 = etree.SubElement(node_tmp, "country") + node_tmp2.set("country", val.value.get("code_country")) + append_fragment(node_tmp2, val.value.get("code_country")) + + node_tmp = etree.SubElement(node, "author-notes") + + for val in vals: + if val.value.get("text_aff"): + node_tmp2 = etree.SubElement(node_tmp, "fn") + node_tmp2.set("fn-type", "other") + aff_ids = normalize_aff_ids(val.value.get("affid")) + if not aff_ids: + continue + aff_id = aff_ids[0] + node_tmp2.set("id", f"fn{aff_id}") + + node_tmp3 = etree.SubElement(node_tmp2, "label") + node_tmp3.text = val.value.get("char") or ("*" * int(aff_id)) + + node_tmp3 = etree.SubElement(node_tmp2, "p") + append_fragment(node_tmp3, val.value.get("text_aff")) + + if article_docx.artdate: + node_tmp = etree.SubElement(node, "pub-date") + node_tmp.set("date-type", "pub") + node_tmp.set("publication-format", "electronic") + + node_tmp2 = etree.SubElement(node_tmp, "day") + node_tmp2.text = article_docx.artdate.strftime("%d") + + node_tmp2 = etree.SubElement(node_tmp, "month") + node_tmp2.text = article_docx.artdate.strftime("%m") + + node_tmp2 = etree.SubElement(node_tmp, "year") + node_tmp2.text = article_docx.artdate.strftime("%Y") + + issue = article_docx.issue + + if issue and (issue.year or issue.month): + node_tmp = etree.SubElement(node, 'pub-date') + node_tmp.set('date-type', 'collection') + node_tmp.set('publication-format', 'electronic') + + if issue.month: + node_tmp2 = etree.SubElement(node_tmp, 'month') + node_tmp2.text = issue.month + + if issue.year: + node_tmp2 = etree.SubElement(node_tmp, 'year') + node_tmp2.text = issue.year + + if issue and issue.volume: + node_tmp = etree.SubElement(node, 'volume') + node_tmp.text = str(issue.volume) + + if issue and issue.number: + node_tmp = etree.SubElement(node, 'issue') + node_tmp.text = str(issue.number) + + if article_docx.dateiso: + node_tmp = etree.SubElement(node, "pub-date") + node_tmp.set("date-type", "collection") + node_tmp.set("publication-format", "electronic") + + if ( + article_docx.dateiso.split("-")[2] + and article_docx.dateiso.split("-")[2] != "00" + ): + node_tmp2 = etree.SubElement(node_tmp, "day") + node_tmp2.text = article_docx.dateiso.split("-")[2] + + if ( + article_docx.dateiso.split("-")[1] + and article_docx.dateiso.split("-")[1] != "00" + ): + node_tmp2 = etree.SubElement(node_tmp, "month") + node_tmp2.text = article_docx.dateiso.split("-")[1] + + node_tmp2 = etree.SubElement(node_tmp, "year") + node_tmp2.text = article_docx.dateiso.split("-")[0] + + if article_docx.elocatid: + node_tmp = etree.SubElement(node, "elocation-id") + node_tmp.text = article_docx.elocatid + + node_tmp = etree.SubElement(node, "history") + + val = next( + ( + b.value["paragraph"] + for b in data_t + if b.block_type == "paragraph" + and b.value.get("label") == "" + ), + None, + ) + + date = extract_date(val) + + if date: + node_tmp2 = etree.SubElement(node_tmp, "date") + node_tmp2.set("date-type", "received") + + node_tmp3 = etree.SubElement(node_tmp2, "day") + node_tmp3.text = date[0] + + node_tmp3 = etree.SubElement(node_tmp2, "month") + node_tmp3.text = date[1] + + node_tmp3 = etree.SubElement(node_tmp2, "year") + node_tmp3.text = date[2] + + val = next( + ( + b.value["paragraph"] + for b in data_t + if b.block_type == "paragraph" + and b.value.get("label") == "" + ), + None, + ) + + date = extract_date(val) + + if date: + node_tmp2 = etree.SubElement(node_tmp, "date") + node_tmp2.set("date-type", "accepted") + + node_tmp3 = etree.SubElement(node_tmp2, "day") + node_tmp3.text = date[0] + + node_tmp3 = etree.SubElement(node_tmp2, "month") + node_tmp3.text = date[1] + + node_tmp3 = etree.SubElement(node_tmp2, "year") + node_tmp3.text = date[2] + + node_tmp = etree.SubElement(node, "permissions") + + if article_docx.license: + node_tmp2 = etree.SubElement(node_tmp, "license") + node_tmp2.set("license-type", "open-access") + node_tmp2.set("{http://www.w3.org/1999/xlink}href", article_docx.license) + node_tmp2.set( + "{http://www.w3.org/XML/1998/namespace}lang", article_docx.language + ) + + node_tmp3 = etree.SubElement(node_tmp2, "license-p") + node_tmp3.text = "Este es un artículo con licencia..." + + vals = [ + b + for b in data_t + if b.block_type == "paragraph" + and b.value.get("label") == "" + ] + + vals2 = [b for b in data_t if _is_abstract_paragraph(b)] + + node_tmp = etree.SubElement(node, "abstract") + + if vals and vals[0]: + node_tmp2 = etree.SubElement(node_tmp, "title") + append_fragment(node_tmp2, vals[0].value.get("paragraph")) + + if vals2 and vals2[0]: + # Encuentra su índice original en article_docx.content + last_index = data_t.index(vals2[0]) + + # Recorre los bloques siguientes + for block in data_t[last_index:]: + if _is_abstract_continuation(block): + subsection = extract_subsection(block.value.get("paragraph")) + + if subsection["title"]: + node_tmp2 = etree.SubElement(node_tmp, "sec") + node_tmp3 = etree.SubElement(node_tmp2, "title") + append_fragment(node_tmp3, subsection["title"]) + node_tmp3 = etree.SubElement(node_tmp2, "p") + append_fragment(node_tmp3, subsection["content"]) + else: + node_tmp2 = etree.SubElement(node_tmp, "p") + append_fragment(node_tmp2, subsection["content"]) + else: + break + + for i, val in enumerate(vals[1:], start=1): + node_tmp = etree.SubElement(node, "trans-abstract") + node_tmp.set( + "{http://www.w3.org/XML/1998/namespace}lang", + vals2[i].value.get("language") or article_docx.language or "en", + ) + + node_tmp2 = etree.SubElement(node_tmp, "title") + append_fragment(node_tmp2, val.value.get("paragraph")) + + last_index = data_t.index(vals2[i]) + + # Recorre los bloques siguientes + for block in data_t[last_index:]: + if _is_abstract_continuation(block): + subsection = extract_subsection(block.value.get("paragraph")) + + if subsection["title"]: + node_tmp2 = etree.SubElement(node_tmp, "sec") + node_tmp3 = etree.SubElement(node_tmp2, "title") + append_fragment(node_tmp3, subsection["title"]) + node_tmp3 = etree.SubElement(node_tmp2, "p") + append_fragment(node_tmp3, subsection["content"]) + else: + node_tmp2 = etree.SubElement(node_tmp, "p") + append_fragment(node_tmp2, subsection["content"]) + else: + break + + vals = [ + b + for b in data_t + if b.block_type == "paragraph" and b.value.get("label") == "" + ] + + vals2 = [ + b + for b in data_t + if b.block_type == "paragraph_with_language" + and b.value.get("label") == "" + ] + + for i, val in enumerate(vals): + node_tmp = etree.SubElement(node, "kwd-group") + node_tmp.set( + "{http://www.w3.org/XML/1998/namespace}lang", + vals2[i].value.get("language"), + ) + + node_tmp2 = etree.SubElement(node_tmp, "title") + append_fragment(node_tmp2, val.value.get("paragraph")) + # node_tmp2.text = val.value.get('paragraph') + + for kw in vals2[i].value.get("paragraph").split(", "): + node_tmp2 = etree.SubElement(node_tmp, "kwd") + append_fragment(node_tmp2, kw) + + countFN = 0 + current_sec = body + node_sec = None + for i, d in enumerate(data): + node = current_sec + + if continue_t: + continue_t = False + continue + + if d["value"]["label"] == "": + val_p = d["value"]["paragraph"].lower() + attrib = {} + if re.search(r"^(intro|sinops|synops)", val_p): + attrib = {"sec-type": "intro"} + elif re.search(r"^(caso|case)", val_p): + attrib = {"sec-type": "cases"} + elif re.search(r"^(conclus|comment|coment)", val_p): + attrib = {"sec-type": "conclusions"} + elif re.search(r"^(discus)", val_p): + attrib = {"sec-type": "discussion"} + elif re.search(r"^(materia)", val_p): + attrib = {"sec-type": "materials"} + elif re.search(r"^(proced|method|métod|metod)", val_p): + attrib = {"sec-type": "methods"} + elif re.search(r"^(result|statement|finding|declara|hallaz)", val_p): + attrib = {"sec-type": "results"} + elif re.search( + r"^(subject|participant|patient|pacient|assunt|sujeto)", val_p + ): + attrib = {"sec-type": "subjects"} + elif re.search(r"^(suplement|material)", val_p): + attrib = {"sec-type": "supplementary-material"} + + current_sec = etree.SubElement(body, "sec", attrib=attrib) + node = current_sec + node_title = etree.SubElement(node, "title") + append_fragment(node_title, d["value"]["paragraph"]) + + subsec = False + + if d["value"]["label"] == "": + subsec = True + node_sec = etree.SubElement(current_sec, "sec") + node_title = etree.SubElement(node_sec, "title") + if re.search(r"^(.*?)$", d["value"]["paragraph"]): + sech = d["value"]["paragraph"] + node_subtitle = etree.SubElement(node_title, "italic") + append_fragment(node_subtitle, sech) + + if d["value"]["label"] == "": + re_search = re.search(r'list list-type="(.*?)"\]', d["value"]["paragraph"]) + list_type = re_search.group(1) if re_search else "bullet" + attrib = {"list-type": list_type} + + if subsec: + node_p = etree.SubElement(node_sec, "p") + node_list = etree.SubElement(node_p, "list", attrib=attrib) + else: + node_p = etree.SubElement(node, "p") + node_list = etree.SubElement(node_p, "list", attrib=attrib) + + content_list = re.search( + r'\[list list-type="[^"]*"\](.*?)\[/list\]', + d["value"]["paragraph"], + re.DOTALL, + ) + content_list = content_list.group(1) if content_list else "" + node_list_text = content_list.replace( + "[list-item]", "

    " + ).replace("[/list-item]", "

    ") + + node_list_text = sanitize_inline_xml_fragment(node_list_text) + node_list_text = etree.fromstring(f"{node_list_text}") + + for child in node_list_text: + node_list.append(child) + + if d["value"]["label"] == "
    are grouped into
    " or d["value"]["label"] == "": + attrib = {"id": d["value"].get("tabid", "")} + + if subsec: + node_p = etree.SubElement(node_sec, "p") + node_table = etree.SubElement(node_p, "table-wrap", attrib=attrib) + else: + node_p = etree.SubElement(node, "p") + node_table = etree.SubElement(node_p, "table-wrap", attrib=attrib) + + node_label = etree.SubElement(node_table, "label") + append_fragment(node_label, d.get("value", {}).get("tablabel")) + + node_caption = etree.SubElement(node_table, "caption") + node_title = etree.SubElement(node_caption, "title") + append_fragment(node_title, d.get("value", {}).get("title")) + + node_table_text = d["value"]["content"] + + # Quitar saltos de línea y espacios extra + node_table_text = re.sub(r"\s*\n\s*", "", node_table_text).replace( + "
    ", "" + ) + node_table_text = re.sub(r"&(?!\w+;|#\d+;)", "&", node_table_text) + + tabla_element = parse_xml_fragment(node_table_text) + + # Ensure
    has proper / structure required by + # the packtools PDF pipeline to extract headers and data rows. + _wrap_table_rows(tabla_element) + + # Insertar en el XML principal + node_table.append(tabla_element) + + node_foot = etree.SubElement(node_p, "table-wrap-foot") + + if d["value"]["label"] == "": + countFN += 1 + node_fn = etree.SubElement( + node_foot, "fn", attrib={"id": f"TFN{str(countFN)}"} + ) + node_fnp = etree.SubElement(node_fn, "p") + append_fragment(node_fnp, d["value"]["paragraph"]) + + if d["value"]["label"] == "": + attrib = {"id": d["value"].get("figid", "")} + + if subsec: + node_p = etree.SubElement(node_sec, "p") + node_fig = etree.SubElement(node_p, "fig", attrib=attrib) + else: + node_p = etree.SubElement(node, "p") + node_fig = etree.SubElement(node_p, "fig", attrib=attrib) + + etree.SubElement(node_fig, "label").text = d["value"].get("figlabel") + node_caption = etree.SubElement(node_fig, "caption") + etree.SubElement(node_caption, "title").text = d["value"].get("title") + + Image = get_image_model() + image_id = d["value"]["image"] + image_obj = Image.objects.get(pk=image_id) + original_filename = os.path.basename(image_obj.title or image_obj.file.name) + + # node_caption = etree.SubElement(node_fig, 'graphic', attrib={'{http://www.w3.org/1999/xlink}ref': f"{d['value']['figid']}.jpeg"}) + node_caption = etree.SubElement( + node_fig, + "graphic", + attrib={"{http://www.w3.org/1999/xlink}href": original_filename}, + ) + + if d["value"]["label"] == "": + node_attrib = etree.SubElement(node_fig, "attrib") + append_fragment(node_attrib, d["value"]["paragraph"]) + + if d["value"]["label"] == "": + attrib = {"id": d["value"].get("eid", "")} + + if subsec: + node_p = etree.SubElement(node_sec, "p") + node_f = etree.SubElement(node_p, "disp-formula", attrib=attrib) + else: + node_p = etree.SubElement(node, "p") + node_f = etree.SubElement(node_p, "disp-formula", attrib=attrib) + + for c in d["value"]["content"]: + if c["type"] == "text": + node_t = etree.SubElement(node_f, "label") + append_fragment(node_t, c["value"]) + if c["type"] == "formula": + _append_formula_fragment(node_f, c["value"]) + + if d["value"]["label"] == "": + attrib = {"id": d["value"].get("eid", "")} + + if subsec: + node_p = etree.SubElement(node_sec, "p") + else: + node_p = etree.SubElement(node, "p") + + for c in d["value"]["content"]: + if c["type"] == "text": + _append_inline_text_fragment(node_p, c["value"]) + if c["type"] == "formula": + node_f = etree.Element("inline-formula", attrib=attrib) + _append_formula_fragment(node_f, c["value"]) + node_p.append(node_f) + + if d["value"]["label"] == "

    ": + if subsec: + node_p = etree.SubElement(node_sec, "p") + else: + node_p = etree.SubElement(node, "p") + + # Apply all xref passes to every paragraph, operating segment-by-segment + # so that citations already marked by tasks.py pre-processing are not + # double-wrapped, and citations in the same paragraph that were missed + # still get processed. + if xref_map: + d["value"]["paragraph"] = _apply_xref_map(d["value"]["paragraph"], xref_map) + d["value"]["paragraph"] = _text_xref_fn(d["value"]["paragraph"]) + d["value"]["paragraph"] = _apply_process_labeled_text(d["value"]["paragraph"], data_back) + + elements = process_special_content(d["value"]["paragraph"], data) + for e in elements: + d["value"]["paragraph"] = d["value"]["paragraph"].replace( + e["label"], + f"{e['label']}", + ) + + append_fragment(node_p, d["value"]["paragraph"]) + + if d["value"]["label"] == "": + if subsec: + node_p = etree.SubElement(node_sec, "p") + else: + node_p = etree.SubElement(node, "p") + + p_text = "" + if "content" in d["value"]: + for val in d["value"]["content"]: + if re.search(r"^(.*?)$", val["value"]): + node_title.text = "" + # ph = val['value'].replace('[style name="italic"]', '').replace('[/style]', '') + ph = val["value"] + node_subtitle = etree.fromstring(f"{ph}") + for child in node_subtitle: + node_title.append(child) + else: + # p_text += val['value'].replace('[style name="italic"]', '').replace('[/style]', '').replace('xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"', '') + p_text += val["value"].replace( + 'xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"', + "", + ) + + node_text = etree.fromstring( + f"{p_text}" + ) + for child in node_text: + node_p.append(child) + + for i, d in enumerate(data_back): + if d["value"]["label"] == "": + node_tit = etree.SubElement(node_reflist, "title") + append_fragment(node_tit, d["value"]["paragraph"]) + if d["value"]["label"] == "

    ": + values = d["value"] + refid = values.get("refid") or f"B{i + 1}" + node_ref = etree.SubElement(node_reflist, "ref", attrib={"id": refid}) + node_mix = etree.SubElement(node_ref, "mixed-citation") + append_fragment(node_mix, values["paragraph"]) + + if values.get("reftype") == "journal": + node_elem = etree.SubElement( + node_ref, + "element-citation", + attrib={"publication-type": values.get("reftype")}, + ) + node_person = etree.SubElement( + node_elem, "person-group", attrib={"person-group-type": "author"} + ) + for a in values["authors"]: + node_name = etree.SubElement(node_person, "name") + node_sname = etree.SubElement(node_name, "surname") + node_gname = etree.SubElement(node_name, "given-names") + append_fragment(node_sname, a["value"]["surname"]) + append_fragment(node_gname, a["value"]["given_names"]) + + append_fragment( + etree.SubElement(node_ref, "article-title"), values["title"] + ) + append_fragment(etree.SubElement(node_ref, "source"), values["source"]) + append_fragment(etree.SubElement(node_ref, "year"), str(values["date"])) + append_fragment( + etree.SubElement(node_ref, "volume"), str(values["vol"]) + ) + append_fragment( + etree.SubElement(node_ref, "issue"), str(values["issue"]) + ) + + if values["fpage"] and values["fpage"][0] == "e": + append_fragment( + etree.SubElement(node_ref, "elocation-id"), values["fpage"] + ) + else: + append_fragment( + etree.SubElement(node_ref, "fpage"), str(values["fpage"]) + ) + append_fragment( + etree.SubElement(node_ref, "lpage"), str(values["lpage"]) + ) + + append_fragment( + etree.SubElement(node_ref, "pub-id", attrib={"pub-id-type": "doi"}), + values["doi"], + ) + + if values["uri"]: + append_fragment( + etree.SubElement( + node_ref, + "ext-link", + attrib={ + "ext-link-type": "uri", + "{http://www.w3.org/1999/xlink}href": values["uri"], + }, + ), + values["uri"], + ) + + if values.get("reftype") == "book": + node_elem = etree.SubElement( + node_ref, + "element-citation", + attrib={"publication-type": values.get("reftype")}, + ) + node_person = etree.SubElement( + node_elem, "person-group", attrib={"person-group-type": "author"} + ) + for a in values["authors"]: + node_name = etree.SubElement(node_person, "name") + node_sname = etree.SubElement(node_name, "surname") + node_gname = etree.SubElement(node_name, "given-names") + append_fragment(node_sname, a["value"]["surname"]) + append_fragment(node_gname, a["value"]["given_names"]) + + append_fragment( + etree.SubElement(node_ref, "part-title"), values["chapter"] + ) + append_fragment(etree.SubElement(node_ref, "source"), values["source"]) + append_fragment( + etree.SubElement(node_ref, "edition"), values["edition"] + ) + append_fragment( + etree.SubElement(node_ref, "publisher-loc"), values["location"] + ) + append_fragment( + etree.SubElement(node_ref, "publisher-name"), values["organization"] + ) + append_fragment(etree.SubElement(node_ref, "year"), str(values["date"])) + append_fragment( + etree.SubElement(node_ref, "fpage"), str(values["fpage"]) + ) + append_fragment( + etree.SubElement(node_ref, "lpage"), str(values["lpage"]) + ) + + if values.get("reftype") == "data": + node_elem = etree.SubElement( + node_ref, + "element-citation", + attrib={"publication-type": values.get("reftype")}, + ) + node_person = etree.SubElement( + node_elem, "person-group", attrib={"person-group-type": "author"} + ) + for a in values["authors"]: + node_name = etree.SubElement(node_person, "name") + node_sname = etree.SubElement(node_name, "surname") + node_gname = etree.SubElement(node_name, "given-names") + append_fragment(node_sname, a["value"]["surname"]) + append_fragment(node_gname, a["value"]["given_names"]) + + append_fragment( + etree.SubElement(node_ref, "data-title"), values["title"] + ) + append_fragment( + etree.SubElement(node_ref, "version"), values["version"] + ) + append_fragment(etree.SubElement(node_ref, "year"), str(values["date"])) + append_fragment(etree.SubElement(node_ref, "source"), values["source"]) + append_fragment( + etree.SubElement(node_ref, "pub-id", attrib={"pub-id-type": "doi"}), + values["doi"], + ) + if values["uri"]: + append_fragment( + etree.SubElement( + node_ref, + "ext-link", + attrib={ + "ext-link-type": "uri", + "{http://www.w3.org/1999/xlink}href": values["uri"], + }, + ), + values["uri"], + ) + + if values.get("reftype") == "webpage": + node_elem = etree.SubElement( + node_ref, + "element-citation", + attrib={"publication-type": values.get("reftype")}, + ) + node_person = etree.SubElement( + node_elem, "person-group", attrib={"person-group-type": "author"} + ) + for a in values["authors"]: + node_name = etree.SubElement(node_person, "name") + node_sname = etree.SubElement(node_name, "surname") + node_gname = etree.SubElement(node_name, "given-names") + append_fragment(node_sname, a["value"]["surname"]) + append_fragment(node_gname, a["value"]["given_names"]) + + append_fragment(etree.SubElement(node_ref, "source"), values["source"]) + if values["uri"]: + append_fragment( + etree.SubElement( + node_ref, + "ext-link", + attrib={ + "ext-link-type": "uri", + "{http://www.w3.org/1999/xlink}href": values["uri"], + }, + ), + values["uri"], + ) + append_fragment( + etree.SubElement(node_ref, "access-date"), values["access_date"] + ) + + if values.get("reftype") == "confproc": + node_elem = etree.SubElement( + node_ref, + "element-citation", + attrib={"publication-type": values.get("reftype")}, + ) + node_person = etree.SubElement( + node_elem, "person-group", attrib={"person-group-type": "author"} + ) + for a in values["authors"]: + node_name = etree.SubElement(node_person, "name") + node_sname = etree.SubElement(node_name, "surname") + node_gname = etree.SubElement(node_name, "given-names") + append_fragment(node_sname, a["value"]["surname"]) + append_fragment(node_gname, a["value"]["given_names"]) + + append_fragment(etree.SubElement(node_ref, "source"), values["source"]) + append_fragment( + etree.SubElement(node_ref, "conf-name"), values["title"] + ) + append_fragment( + etree.SubElement(node_ref, "conf-num"), str(values["issue"]) + ) + append_fragment( + etree.SubElement(node_ref, "conf-date"), str(values["date"]) + ) + append_fragment( + etree.SubElement(node_ref, "conf-loc"), values["location"] + ) + append_fragment( + etree.SubElement(node_ref, "publisher-loc"), values["org_location"] + ) + append_fragment( + etree.SubElement(node_ref, "publisher-name"), values["organization"] + ) + append_fragment(etree.SubElement(node_ref, "page"), values["pages"]) + + if values.get("reftype") == "thesis": + node_elem = etree.SubElement( + node_ref, + "element-citation", + attrib={"publication-type": values.get("reftype")}, + ) + node_person = etree.SubElement( + node_elem, "person-group", attrib={"person-group-type": "author"} + ) + for a in values["authors"]: + node_name = etree.SubElement(node_person, "name") + node_sname = etree.SubElement(node_name, "surname") + node_gname = etree.SubElement(node_name, "given-names") + append_fragment(node_sname, a["value"]["surname"]) + append_fragment(node_gname, a["value"]["given_names"]) + + append_fragment(etree.SubElement(node_ref, "source"), values["source"]) + append_fragment( + etree.SubElement(node_ref, "publisher-loc"), values["org_location"] + ) + append_fragment( + etree.SubElement(node_ref, "publisher-name"), values["organization"] + ) + append_fragment(etree.SubElement(node_ref, "year"), str(values["date"])) + append_fragment(etree.SubElement(node_ref, "page"), values["pages"]) + + # Convertir a una cadena XML + xml_como_texto = etree.tostring(root, pretty_print=True, encoding="unicode") + + return xml_como_texto, data diff --git a/sps/xref.py b/sps/xref.py new file mode 100644 index 0000000..3362db0 --- /dev/null +++ b/sps/xref.py @@ -0,0 +1,1032 @@ +""" +Cross-reference (xref) linking for the DOCX → SPS XML pipeline. + +Official convention +------------------- +- Each reference entry in the reference list receives a bookmark named + ``xref_B{n}`` (1-indexed, n = position in the reference list). +- Each in-text citation becomes a Word internal hyperlink whose anchor + points to the corresponding ``xref_B{n}`` bookmark. + +This convention allows: +- Clicking a citation in Word → jumps to the reference entry. +- Clicking the reference entry bookmark → jumps back (if a reverse + hyperlink is added by the editor). + +Supported citation styles (auto-detected for unmarked documents): +- ABNT : (Autor, 2020) or (Autor et al., 2020) +- Vancouver bracket : [1] or [7,8] or [3-5] +- Vancouver superscript: runs with font.superscript == True containing digits + +Validation rules: +- ERROR : a hyperlink points to a bookmark that does not exist. +- WARNING : a bookmark has no corresponding hyperlink (uncited reference). +""" + +import copy +import re +import unicodedata + +from docx import Document +from docx.oxml import OxmlElement +from docx.oxml.ns import qn + +BOOKMARK_PREFIX = "xref_B" + +_REF_HEADINGS = { + "references", + "referências", + "referências bibliográficas", + "referencias", + "referencias bibliográficas", + "bibliography", + "bibliografia", +} + +_STOP_HEADINGS = { + "figures captions", + "figure captions", + "figures", + "supplementary material", + "supplementary materials", + "appendix", + "appendices", + "supporting information", + "acknowledgements", + "acknowledgments", + "agradecimentos", + "material suplementar", + "notas", + "notes", + # author/editor metadata sections + "author contributions", + "contribuições dos autores", + "contribuciones de los autores", + "data availability", + "data availability statement", + "disponibilidade dos dados", + "funding", + "financiamento", + "conflict of interest", + "conflicts of interest", + "conflito de interesses", + "declaration of competing interest", + "editors", + "editor associado", + "editor científico", + "associate editor", + "scientific editor", +} + +_ALLCAPS_STOP_RE = re.compile(r'^[A-ZÁÉÍÓÚÀÂÊÔÃÕÜÇÄÖÏËØÅÆŒ\s\-]{4,60}$') + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + +def is_marked(doc: Document) -> bool: + """Return True if *doc* contains xref_B* bookmarks AND hyperlinks.""" + xml = doc.element.xml + has_bk = bool(re.search(rf'w:name="{BOOKMARK_PREFIX}\d+"', xml)) + has_hl = bool(re.search(rf'w:anchor="{BOOKMARK_PREFIX}\d+"', xml)) + return has_bk and has_hl + + +def validate_marks(doc: Document) -> dict: + """ + Validate consistency of xref markup. + + Returns a dict:: + + { + "valid": bool, # False when any hyperlink is orphaned + "bookmarks": set[str], # all xref_B* bookmarks found + "hyperlinks": set[str], # all xref_B* anchors found + "orphaned_bookmarks": list, # bookmarks without a citation (warnings) + "orphaned_hyperlinks": list, # citations without a reference (errors) + "warnings": list[str], + "errors": list[str], + } + """ + xml = doc.element.xml + bookmarks = set(re.findall(rf'w:name="({BOOKMARK_PREFIX}\d+)"', xml)) + hyperlinks = set(re.findall(rf'w:anchor="({BOOKMARK_PREFIX}\d+)"', xml)) + + orphaned_bk = sorted(bookmarks - hyperlinks) + orphaned_hl = sorted(hyperlinks - bookmarks) + + warnings = [f"Reference {b} has no in-text citation." for b in orphaned_bk] + errors = [f"Citation links to {h} but no matching reference bookmark found." for h in orphaned_hl] + + return { + "valid": len(orphaned_hl) == 0, + "bookmarks": bookmarks, + "hyperlinks": hyperlinks, + "orphaned_bookmarks": orphaned_bk, + "orphaned_hyperlinks": orphaned_hl, + "warnings": warnings, + "errors": errors, + } + + +def read_marks(doc: Document) -> list: + """ + Extract xref data from a marked document. + + Returns a list of dicts (one per reference), ordered by bookmark index:: + + [ + { + "rid": "B1", + "bookmark": "xref_B1", + "ref_text": "AUTOR, A. 2020. Título...", + "citations": ["(Autor, 2020)", ...], # in-text citation texts + }, + ... + ] + """ + xml = doc.element.xml + + # Collect all bookmark names present + bk_names = sorted( + set(re.findall(rf'w:name="({BOOKMARK_PREFIX}\d+)"', xml)), + key=lambda s: int(s[len(BOOKMARK_PREFIX):]), + ) + + # Map anchor → list of citation texts extracted from hyperlinks + citation_map: dict[str, list[str]] = {b: [] for b in bk_names} + + # Scan ALL paragraphs (including those inside table cells) + for p_elem in doc.element.body.iter(qn("w:p")): + p_xml = p_elem.xml + for m in re.finditer( + rf']+w:anchor="({BOOKMARK_PREFIX}\d+)"[^>]*>(.*?)', + p_xml, + re.DOTALL, + ): + anchor = m.group(1) + inner = m.group(2) + # Extract plain text from the hyperlink's runs and unescape XML entities + texts = re.findall(r']*>([^<]*)', inner) + citation_text = "".join(texts).strip().replace("&", "&").replace("<", "<").replace(">", ">").replace(""", '"').replace("'", "'") + if anchor in citation_map: + citation_map[anchor].append(citation_text) + + # Map bookmark → reference paragraph text + ref_paragraphs = _find_references_section(doc) + ref_text_map: dict[str, str] = {} + for idx, (_, para) in enumerate(ref_paragraphs, start=1): + bk = f"{BOOKMARK_PREFIX}{idx}" + ref_text_map[bk] = para.text.strip() + + result = [] + for bk in bk_names: + n = bk[len(BOOKMARK_PREFIX):] + result.append({ + "rid": f"B{n}", + "bookmark": bk, + "ref_text": ref_text_map.get(bk, ""), + "citations": citation_map.get(bk, []), + }) + return result + + +def apply_xml_xrefs_to_docx(doc: Document, xml_tree) -> Document: + """ + Add Word internal hyperlinks/bookmarks to a DOCX rendered from SPS XML. + + The packtools PDF DOCX renderer preserves the visible text of + ``xref[@ref-type='bibr']`` but not the link semantics. This restores the + same ``xref_Bn`` bookmark convention used by ``mark_references``. + """ + references = [] + for ref in xml_tree.xpath(".//ref[@id]"): + mixed = " ".join(ref.xpath(".//mixed-citation//text()")).strip() + if mixed: + references.append({"rid": ref.get("id"), "text": _normalize_ws(mixed)}) + + citations = [] + for xref in xml_tree.xpath(".//xref[@ref-type='bibr'][@rid]"): + text = " ".join(xref.xpath(".//text()")).strip() + if text: + citations.append({"rid": xref.get("rid"), "text": _normalize_ws(text)}) + + if not references or not citations: + return doc + + next_id = _next_bookmark_id(doc) + bookmarked = set(re.findall(rf'w:name="({BOOKMARK_PREFIX}\d+)"', doc.element.xml)) + ref_paragraphs = list(_iter_paragraphs(doc)) + for ref in references: + anchor = _anchor_from_rid(ref["rid"]) + if not anchor or anchor in bookmarked: + continue + para = _find_paragraph_containing(ref_paragraphs, ref["text"]) + if para is None: + continue + _add_bookmark_to_para(para, anchor, next_id) + bookmarked.add(anchor) + next_id += 1 + + for para in _iter_paragraphs(doc): + spans = [] + para_text = para.text + if not para_text: + continue + for citation in citations: + anchor = _anchor_from_rid(citation["rid"]) + if not anchor or anchor not in bookmarked: + continue + start = para_text.find(citation["text"]) + if start < 0: + continue + end = start + len(citation["text"]) + spans.append((start, end, anchor, citation["text"])) + if spans: + _insert_hyperlinks_plain(para, spans) + + return doc + + +def mark_references(doc: Document) -> Document: + """ + Auto-detect citations and add xref markup to *doc*. + + 1. Adds ``xref_B{n}`` bookmarks to each reference entry. + 2. Detects the citation style (ABNT, Vancouver bracket, superscript). + 3. Wraps in-text citations in internal hyperlinks pointing to the + corresponding bookmark. + + Returns the modified Document (same object, mutated in place). + """ + refs = _find_references_section(doc) + if not refs: + return doc + + # Step 1 — bookmark each reference + bk_id_start = _next_bookmark_id(doc) + for offset, (_, para) in enumerate(refs): + bk_name = f"{BOOKMARK_PREFIX}{offset + 1}" + _add_bookmark_to_para(para, bk_name, bk_id_start + offset) + + # Build reference index for matching + ref_index = _build_ref_index(refs) + + # Step 2 — detect style and find citations + style = _detect_citation_style(doc) + + if style == "vancouver_bracket": + citations = _find_citations_bracket(doc) + elif style == "vancouver_superscript": + citations = _find_citations_superscript(doc) + else: + citations = _find_citations_abnt(doc, ref_index) + + # Step 3 — insert hyperlinks + for para, spans in citations.items(): + _insert_hyperlinks(para, spans) + + return doc + + +def _normalize_ws(text: str) -> str: + return re.sub(r"\s+", " ", text or "").strip() + + +def _normalize_match_text(text: str) -> str: + text = _normalize_ws(text) + text = re.sub(r"\s+([,.;:?!\)])", r"\1", text) + text = re.sub(r"([\(\[])\s+", r"\1", text) + return text.rstrip(".") + + +def _iter_paragraphs(doc: Document): + for paragraph in doc.paragraphs: + yield paragraph + for table in doc.tables: + for row in table.rows: + for cell in row.cells: + for paragraph in cell.paragraphs: + yield paragraph + + +def _anchor_from_rid(rid: str) -> str | None: + first = (rid or "").split() + if not first: + return None + match = re.match(r"^B(\d+)$", first[0]) + if not match: + return None + return f"{BOOKMARK_PREFIX}{match.group(1)}" + + +def _find_paragraph_containing(paragraphs, target: str): + target_norm = _normalize_match_text(target) + if not target_norm: + return None + for para in paragraphs: + para_norm = _normalize_match_text(para.text) + if not para_norm: + continue + if target_norm in para_norm or para_norm in target_norm: + return para + target_prefix = target_norm[:80] + for para in paragraphs: + para_norm = _normalize_match_text(para.text) + if target_prefix and para_norm and target_prefix in para_norm: + return para + return None + + +# --------------------------------------------------------------------------- +# Detection helpers +# --------------------------------------------------------------------------- + +def _detect_citation_style(doc: Document) -> str: + """Return 'abnt', 'vancouver_bracket', or 'vancouver_superscript'.""" + body_paras = _body_paragraphs(doc) + full_text = " ".join(p.text for p in body_paras) + + # Bracket citations [1] or [1,2] are the most unambiguous signal. + brackets = re.findall(r'\[\d+(?:[,\-]\d+)*\]', full_text) + if len(brackets) >= 3: + return "vancouver_bracket" + + # Superscript digit runs — require a high count to avoid mistaking + # footnote markers or ordinals in ABNT documents. + sup_count = sum( + 1 + for para in body_paras + for run in para.runs + if run.font.superscript and re.fullmatch(r'[\d,\s\-]+', run.text.strip()) + ) + abnt_count = len(re.findall( + r'\([A-ZÁÉÍÓÚÀÂÊÔÃÕÜÇ][^()]{2,80}\d{4}[^()]*\)', full_text + )) + # Declare superscript only when it clearly dominates over ABNT matches. + if sup_count >= 10 and sup_count > abnt_count * 3: + return "vancouver_superscript" + + return "abnt" + + +def _iter_all_paragraphs(doc: Document): + """Yield all Paragraph objects in document order, including inside tables.""" + from docx.text.paragraph import Paragraph as _Para + for p_elem in doc.element.body.iter(qn("w:p")): + yield _Para(p_elem, doc) + + +_METADATA_RE = re.compile( + r'^(?:received|accepted|published|available\s+at|doi\s*:|https?://)', + re.IGNORECASE, +) + +_YEAR_RE_SIMPLE = re.compile(r'\b(?:1[89]|20)\d{2}\b') + +def _find_references_section(doc: Document) -> list: + """Return list of (paragraph_index, paragraph) for reference entries.""" + in_refs = False + refs = [] + for i, para in enumerate(_iter_all_paragraphs(doc)): + text = para.text.strip() + text_lower = text.lower().rstrip(":.") + if text_lower in _REF_HEADINGS: + in_refs = True + continue + if not in_refs: + continue + # Stop at known post-reference section headings + if text_lower in _STOP_HEADINGS: + break + # Stop at Word heading styles (Heading 1/2/3/...) + style_name = (para.style.name or '') if para.style else '' + if re.match(r'heading\s*\d', style_name, re.IGNORECASE): + break + # Stop at ALL-CAPS short paragraphs without a year — section headings + # like "CONTRIBUIÇÕES DOS AUTORES", "EDITOR ASSOCIADO", etc. + if (text and len(text) <= 60 + and _ALLCAPS_STOP_RE.match(text) + and not _YEAR_RE_SIMPLE.search(text)): + break + if text and not _METADATA_RE.match(text): + refs.append((i, para)) + return refs + + +def _build_ref_index(refs: list) -> list: + """Return list of (n, first_author_normalized, year, para) for ABNT matching.""" + index = [] + year_re = re.compile(r'\b((?:1[89]|20)\d{2}[a-z]?)\b') + for n, (_, para) in enumerate(refs, start=1): + text = para.text.strip() + year_m = year_re.search(text) + year = year_m.group(1) if year_m else "" + first_author = _normalize(_first_surname(text)) + index.append((n, first_author, year, para)) + return index + + +def _first_surname(ref_text: str) -> str: + """Extract the first author surname from a reference string.""" + # ABNT: SOBRENOME, Iniciais. → first word before comma + # Vancouver: Sobrenome AB, ... → first word + m = re.match(r'^([A-ZÁÉÍÓÚÀÂÊÔÃÕÜÇÄÖÏËØÅÆŒ][A-ZÁÉÍÓÚÀÂÊÔÃÕÜÇÄÖÏËØÅÆŒa-záéíóúàâêôãõüçäöïëøåæœ\-]+)', ref_text.strip()) + return m.group(1) if m else ref_text[:10] + + +def _normalize(text: str) -> str: + """Lowercase + remove accents for fuzzy comparison.""" + nfkd = unicodedata.normalize("NFKD", text) + return "".join(c for c in nfkd if not unicodedata.combining(c)).lower() + + +def _body_paragraphs(doc: Document) -> list: + """Return paragraphs that belong to the article body (before references).""" + body = [] + for para in _iter_all_paragraphs(doc): + if para.text.strip().lower() in _REF_HEADINGS: + break + body.append(para) + return body + + +# --------------------------------------------------------------------------- +# Citation finders — return {para: [(start, end, anchor), ...]} +# --------------------------------------------------------------------------- + +def _find_citations_bracket(doc: Document) -> dict: + """Find [n] and [n,m] citations and map them to xref_B* anchors.""" + result: dict = {} + pattern = re.compile(r'\[(\d+(?:[,\-]\d+)*)\]') + + for para in _body_paragraphs(doc): + text = para.text + spans = [] + for m in pattern.finditer(text): + numbers = _expand_range(m.group(1)) + for n in numbers: + anchor = f"{BOOKMARK_PREFIX}{n}" + spans.append((m.start(), m.end(), anchor, m.group(0))) + if spans: + result[para] = spans + return result + + +def _find_citations_superscript(doc: Document) -> dict: + """Find superscript-number citations and map them to xref_B* anchors.""" + result: dict = {} + + for para in _body_paragraphs(doc): + spans = [] + pos = 0 + for run in para.runs: + run_text = run.text + run_end = pos + len(run_text) + if run.font.superscript and re.fullmatch(r'[\d,\s\-]+', run_text.strip()): + # Strip leading/trailing commas that Word sometimes includes + # in the same superscript run as punctuation separators. + clean = run_text.strip().strip(',').strip() + numbers = _expand_range(clean.replace(" ", "")) + for n in numbers: + anchor = f"{BOOKMARK_PREFIX}{n}" + spans.append((pos, run_end, anchor, clean)) + pos = run_end + if spans: + result[para] = spans + return result + + +def _find_citations_abnt(doc: Document, ref_index: list) -> dict: + """ + Find ABNT citations and match against ref_index: + - Parenthetical: (Author, 2020) or (Author et al., 2020; Author2, 2021) + - Narrative: Author (2020) or Author et al. (2020) or Author and Author (2020) + - Plain text: Author Year (e.g. "see Bergstrom 1995") + """ + result: dict = {} + paren_re = re.compile( + r'\(([A-ZÁÉÍÓÚÀÂÊÔÃÕÜÇ][^\(\)]{2,100}\d{4}[^\(\)]*)\)', + re.UNICODE, + ) + year_re = re.compile(r'\b(1[89]\d{2}|20\d{2})\b') + + # Surname token: handles hyphen-compounds with optional space (e.g. "Ilkiu-Borges" or "Ilkiu -Borges") + _sname = ( + r'[A-ZÁÉÍÓÚÀÂÊÔÃÕÜÇÄÖÏËØÅÆŒ][A-ZÁÉÍÓÚÀÂÊÔÃÕÜÇÄÖÏËØÅÆŒa-záéíóúàâêôãõüçäöïëøåæœ]+' + r'(?:\s*-\s*[A-ZÁÉÍÓÚÀÂÊÔÃÕÜÇÄÖÏËØÅÆŒa-záéíóúàâêôãõüçäöïëøåæœ]+)*' + ) + narrative_re = re.compile( + r'(' + _sname + r'(?:\s+(?:and|&)\s+' + _sname + r')*(?:\s+et\s+al\.)?)' + r'\s*\((\d{4}[a-z]?(?:,\s*\d{4}[a-z]?)*)\)', + re.UNICODE, + ) + # Plain text: Surname (and Surname)* Year without parentheses (e.g. "Bergstrom 1995", "Boadway and Keen 1996") + plain_text_re = re.compile( + r'\b(' + _sname + r'(?:\s+(?:and|&)\s+' + _sname + r')*)\s+((?:1[89]|20)\d{2}[a-z]?)\b', + re.UNICODE, + ) + + for para in _body_paragraphs(doc): + text = para.text + spans = [] + covered: set[tuple[int, int]] = set() + + # 1. Parenthetical citations: (Author, year) — split on ";" for multiple + for m in paren_re.finditer(text): + inner = m.group(1) + parts = [p.strip() for p in inner.split(";")] + for part in parts: + year_m = year_re.search(part) + if not year_m: + continue + year = year_m.group(1) + surname_m = re.match(r'([A-ZÁÉÍÓÚÀÂÊÔÃÕÜÇ][^\s,]+)', part) + if not surname_m: + continue + surname = _normalize(surname_m.group(1)) + anchor = _match_abnt(surname, year, ref_index) + if anchor and (m.start(), m.end()) not in covered: + spans.append((m.start(), m.end(), anchor, m.group(0))) + covered.add((m.start(), m.end())) + + # 2. Narrative citations: Author (year) — not already covered by parenthetical + for m in narrative_re.finditer(text): + if (m.start(), m.end()) in covered: + continue + author_part = m.group(1).strip() + years_str = m.group(2) + # Extract first surname (strip et al. first) + author_clean = re.sub(r'\s+et\s+al\.', '', author_part) + first_token = re.match(r'([^\s]+)', author_clean) + if not first_token: + continue + surname = _normalize(first_token.group(1)) + # Try each year in the citation until one matches (handles "Author (1976, 1984, 1985)") + anchor = None + for yr in re.findall(r'\d{4}[a-z]?', years_str): + anchor = _match_abnt(surname, yr, ref_index) + if anchor: + break + if anchor and (m.start(), m.end()) not in covered: + spans.append((m.start(), m.end(), anchor, m.group(0))) + covered.add((m.start(), m.end())) + + # 3. Plain text citations: Author (and Author)* Year — not already covered + for m in plain_text_re.finditer(text): + if any(m.start() >= s and m.end() <= e for s, e in covered): + continue + author_part = m.group(1).strip() + year = m.group(2) + first_token = re.match(r'([^\s]+)', author_part) + if not first_token: + continue + surname = _normalize(first_token.group(1)) + anchor = _match_abnt(surname, year, ref_index) + if anchor and (m.start(), m.end()) not in covered: + spans.append((m.start(), m.end(), anchor, m.group(0))) + covered.add((m.start(), m.end())) + + if spans: + result[para] = spans + return result + + +def _match_abnt(surname: str, year: str, ref_index: list) -> str | None: + """Return xref_Bn for the best match, or None.""" + skey = surname[:5] + year_plain = year[:4] + # Exact match first (preserves 2004a vs 2004b disambiguation) + for n, first_author, ref_year, _ in ref_index: + if ref_year == year and first_author.startswith(skey): + return f"{BOOKMARK_PREFIX}{n}" + # Fallback: compare first 4 chars (handles refs stored without suffix) + for n, first_author, ref_year, _ in ref_index: + if ref_year[:4] == year_plain and first_author.startswith(skey): + return f"{BOOKMARK_PREFIX}{n}" + return None + + +def _expand_range(token: str) -> list[int]: + """'3,5' → [3,5]; '7-9' → [7,8,9]; '2' → [2].""" + numbers = [] + for part in token.split(","): + part = part.strip() + if "-" in part: + a, b = part.split("-", 1) + try: + numbers.extend(range(int(a), int(b) + 1)) + except ValueError: + pass + else: + try: + numbers.append(int(part)) + except ValueError: + pass + return numbers + + +# --------------------------------------------------------------------------- +# XML manipulation +# --------------------------------------------------------------------------- + +def _next_bookmark_id(doc: Document) -> int: + """Return an id value safe to use for new bookmarks.""" + existing = re.findall(r'w:id="(\d+)"', doc.element.xml) + return max((int(i) for i in existing), default=0) + 1 + + +def _add_bookmark_to_para(para, name: str, bk_id: int): + """Wrap the paragraph content in a named bookmark.""" + p = para._p + + bk_start = OxmlElement("w:bookmarkStart") + bk_start.set(qn("w:id"), str(bk_id)) + bk_start.set(qn("w:name"), name) + + bk_end = OxmlElement("w:bookmarkEnd") + bk_end.set(qn("w:id"), str(bk_id)) + + p.insert(0, bk_start) + p.append(bk_end) + + +def _make_text_run(text: str, template_run=None): + r = copy.deepcopy(template_run) if template_run is not None else OxmlElement("w:r") + for child in list(r): + if child.tag != qn("w:rPr"): + r.remove(child) + t = OxmlElement("w:t") + t.text = text + if text.startswith(" ") or text.endswith(" "): + t.set("{http://www.w3.org/XML/1998/namespace}space", "preserve") + r.append(t) + return r + + +def _insert_hyperlinks_plain(para, spans: list): + full_text = para.text + if not full_text: + return + + unique_spans = [] + last_end = -1 + seen = set() + for start, end, anchor, text in sorted(spans, key=lambda s: (s[0], s[1])): + key = (start, end, anchor) + if key in seen or start < last_end: + continue + seen.add(key) + unique_spans.append((start, end, anchor, text)) + last_end = end + if not unique_spans: + return + + p = para._p + run_segments = [] + for child in p: + tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag + if tag in {"r", "hyperlink"}: + run_segments.append(child) + template_run = next( + (elem for elem in run_segments if elem.tag.split("}")[-1] == "r"), + None, + ) + for elem in run_segments: + if elem in p: + p.remove(elem) + + ppr = p.find(qn("w:pPr")) + insert_after = ppr if ppr is not None else None + insert_pos = 0 + + def insert(elem): + nonlocal insert_after, insert_pos + if insert_after is not None: + insert_after.addnext(elem) + insert_after = elem + else: + p.insert(insert_pos, elem) + insert_pos += 1 + + cursor = 0 + for start, end, anchor, text in unique_spans: + if start > cursor: + insert(_make_text_run(full_text[cursor:start], template_run)) + insert(_make_hyperlink(anchor, full_text[start:end], template_run)) + cursor = end + if cursor < len(full_text): + insert(_make_text_run(full_text[cursor:], template_run)) + + +def _insert_hyperlinks(para, spans: list): + """ + Replace citation text in *para* with internal hyperlinks. + + *spans* is a list of (start, end, anchor, original_text) tuples, where + start/end are character offsets in ``para.text``. + Multiple citations pointing to the same span are merged into separate + hyperlinks inserted consecutively. + """ + # Deduplicate spans on (start, end) keeping first match only + seen = set() + unique_spans = [] + for span in sorted(spans, key=lambda s: s[0]): + key = (span[0], span[1]) + if key not in seen: + seen.add(key) + unique_spans.append(span) + + # Rebuild paragraph XML run-by-run, inserting hyperlinks at citation positions + p = para._p + full_text = para.text + + # Collect (run_element, run_start, run_end) from current runs + run_segments = [] + pos = 0 + for child in p: + tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag + if tag == "r": + t_elem = child.find(qn("w:t")) + text = t_elem.text if t_elem is not None and t_elem.text else "" + run_segments.append((child, pos, pos + len(text))) + pos += len(text) + elif tag == "hyperlink": + # Already a hyperlink — count its text length + inner_text = "".join( + (t.text or "") for t in child.iter(qn("w:t")) + ) + run_segments.append((child, pos, pos + len(inner_text))) + pos += len(inner_text) + + if not run_segments: + return + + # Build list of "what goes where" in character-offset order + # Each item: ('run', elem) or ('hyperlink', anchor, text, template_run) + events = [] # (char_offset, type, ...) + + # Mark citation zones + citation_zones = {(s, e): (anchor, txt) for s, e, anchor, txt in unique_spans} + + offset = 0 + seg_idx = 0 + while seg_idx < len(run_segments) and offset < len(full_text): + elem, seg_start, seg_end = run_segments[seg_idx] + tag = elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag + + # Check if a citation zone starts here + matched_zone = None + for (z_start, z_end), (anchor, cit_text) in citation_zones.items(): + if seg_start <= z_start < seg_end or (z_start <= seg_start < z_end): + matched_zone = (z_start, z_end, anchor, cit_text) + break + + if matched_zone is None or tag == "hyperlink": + events.append(("keep", elem)) + seg_idx += 1 + continue + + z_start, z_end, anchor, cit_text = matched_zone + + # Split first run: extract text before the citation starts + t_node = elem.find(qn("w:t")) + run_text = t_node.text if t_node is not None and t_node.text else "" + before = run_text[:max(0, z_start - seg_start)] + if before: + r_before = copy.deepcopy(elem) + t = r_before.find(qn("w:t")) + t.text = before + if before.startswith(" ") or before.endswith(" "): + t.set("{http://www.w3.org/XML/1998/namespace}space", "preserve") + events.append(("keep", r_before)) + + # Advance seg_idx to the last run that overlaps this zone. + # Citations like "Costa et al. (2020)" span multiple runs; + # without this loop only the first run portion would be hyperlinked. + while seg_idx + 1 < len(run_segments) and run_segments[seg_idx + 1][1] < z_end: + seg_idx += 1 + + # Extract text after the citation ends from the last run in the zone + last_elem, last_start, _ = run_segments[seg_idx] + last_t = last_elem.find(qn("w:t")) + last_text = last_t.text if last_t is not None and last_t.text else "" + after = last_text[max(0, z_end - last_start):] + + # Emit hyperlink with full citation text (first run used as style template) + events.append(("hyperlink", anchor, cit_text, elem)) + + if after: + r_after = copy.deepcopy(last_elem) + t = r_after.find(qn("w:t")) + if t is not None: + t.text = after + if after.startswith(" ") or after.endswith(" "): + t.set("{http://www.w3.org/XML/1998/namespace}space", "preserve") + events.append(("keep", r_after)) + + del citation_zones[(z_start, z_end)] + seg_idx += 1 + + # Remaining segments + for elem, _, _ in run_segments[seg_idx:]: + events.append(("keep", elem)) + + # Remove old run/hyperlink children from paragraph + for elem, _, _ in run_segments: + if elem in p: + p.remove(elem) + + # Re-insert in order + insert_pos = 0 + # Find insertion point (after pPr if present) + ppr = p.find(qn("w:pPr")) + insert_after = ppr if ppr is not None else None + + for event in events: + if event[0] == "keep": + elem = event[1] + if insert_after is not None: + insert_after.addnext(elem) + insert_after = elem + else: + p.insert(insert_pos, elem) + insert_pos += 1 + else: + _, anchor, cit_text, template_run = event + hl = _make_hyperlink(anchor, cit_text, template_run) + if insert_after is not None: + insert_after.addnext(hl) + insert_after = hl + else: + p.insert(insert_pos, hl) + insert_pos += 1 + + +def build_text_xref_replacer(doc: Document): + """ + Build a callable that tags 'Author (year)' narrative citations with . + Builds the reference lookup directly from the reference list section in *doc*, + assigning B1..Bn by position (consistent with read_marks / xml.py convention). + Returns: apply(text: str) -> str + """ + refs = _find_references_section(doc) + ref_list = [ + {'rid': f'B{i + 1}', 'ref_text': para.text.strip()} + for i, (_, para) in enumerate(refs) + ] + return _make_text_xref_fn(ref_list) + + +def make_text_xref_fn_from_refs(ref_items: list): + """ + Build a narrative xref replacer from reference dicts with keys: + {'rid'|'refid': 'Bn', 'ref_text'|'paragraph': '...'}. + Returns: apply(text: str) -> str + """ + return _make_text_xref_fn(ref_items) + + +def _make_text_xref_fn(ref_list: list): + """Build the 'Author (year)' replacer function from a list of reference dicts.""" + # Year regex includes optional letter suffix (e.g. 2004a, 2004b) + _year_re = re.compile(r'\b((?:1[89]|20)\d{2}[a-z]?)\b') + # Tuples: (skey, year_with_suffix, rid, full_ref_text_normalized) for compound-author lookup + ref_entries: list[tuple[str, str, str, str]] = [] + # Simple primary lookup: first match wins + ref_lookup: dict[tuple[str, str], str] = {} + + for i, item in enumerate(ref_list): + rid = item.get('rid') or item.get('refid') or f'B{i + 1}' + text = item.get('ref_text') or item.get('paragraph') or '' + if not text: + continue + skey = _normalize(_first_surname(text))[:5] + norm_text = _normalize(text) + for year in _year_re.findall(text)[:4]: + ref_entries.append((skey, year, rid, norm_text)) + if (skey, year) not in ref_lookup: + ref_lookup[(skey, year)] = rid + + if not ref_entries: + return lambda t: t + + def _lookup(skey: str, year: str, extra_skeys: list[str]) -> str | None: + """Find best rid: prefer entries containing all author surnames.""" + candidates = [(rid, norm) for s, y, rid, norm in ref_entries if s == skey and y == year] + if not candidates: + return None + if len(candidates) == 1 or not extra_skeys: + return candidates[0][0] + # Prefer candidate whose text contains the extra authors + for rid, norm in candidates: + if all(sk in norm for sk in extra_skeys): + return rid + return candidates[0][0] + + # Reusable surname token: handles "Ilkiu-Borges" and "Ilkiu -Borges" (space before hyphen) + _sname = ( + r'[A-ZÁÉÍÓÚÀÂÊÔÃÕÜÇÄÖÏËØÅÆŒ][A-ZÁÉÍÓÚÀÂÊÔÃÕÜÇÄÖÏËØÅÆŒa-záéíóúàâêôãõüçäöïëøåæœ]+' + r'(?:\s*-\s*[A-ZÁÉÍÓÚÀÂÊÔÃÕÜÇÄÖÏËØÅÆŒa-záéíóúàâêôãõüçäöïëøåæœ]+)*' + ) + # et al. can appear as plain text or wrapped in tags + _etal = r'(?:\s+(?:et\s+al\.|et\s+al\.?\.?))?' + # Match: Surname [and/& Surname]* [et al.] (year[a-z]?[, year[a-z]?]*) + _narrative_re = re.compile( + r'(' + _sname + r'(?:\s+(?:and|&)\s+' + _sname + r')*' + _etal + r')' + r'\s*\((\d{4}[a-z]?(?:,\s*\d{4}[a-z]?)*)\)', + re.UNICODE, + ) + _split_re = re.compile(r'(]*>.*?)', re.DOTALL) + _etal_strip = re.compile(r'\s+(?:et\s+al\.|et\s+al\.?\.?)') + + def _replace(m: re.Match) -> str: + full = m.group(0) + author_part = m.group(1).strip() + years_str = m.group(2) + # Remove et al., then split on and/& to get individual author tokens + author_clean = _etal_strip.sub('', author_part) + author_tokens = re.split(r'\s+(?:and|&)\s+', author_clean) + skeys = [_normalize(t.split()[0])[:5] for t in author_tokens if t.strip()] + if not skeys: + return full + primary_skey = skeys[0] + extra_skeys = skeys[1:] + rids: list[str] = [] + for year in re.findall(r'\d{4}[a-z]?', years_str): + rid = _lookup(primary_skey, year, extra_skeys) + if rid and rid not in rids: + rids.append(rid) + if not rids: + return full + return f'{full}' + + _paren_inner_re = re.compile( + r'\(([A-ZÁÉÍÓÚÀÂÊÔÃÕÜÇÄÖÏËØÅÆŒ][^\(\)]{2,200}\d{4}[^\(\)]*)\)', + re.UNICODE, + ) + _paren_year_re = re.compile(r'\b(1[89]\d{2}|20\d{2})\b') + _paren_author_re = re.compile(r'([A-ZÁÉÍÓÚÀÂÊÔÃÕÜÇÄÖÏËØÅÆŒ][^\s,;]+)') + + def _replace_paren(m: re.Match) -> str: + full = m.group(0) + inner = m.group(1) + parts = [p.strip() for p in re.split(r'[;,]\s*(?=[A-ZÁÉÍÓÚÀÂÊÔÃÕÜÇÄÖÏËØÅÆŒ])', inner)] + rids: list[str] = [] + for part in parts: + yr_m = _paren_year_re.search(part) + if not yr_m: + continue + au_m = _paren_author_re.match(part) + if not au_m: + continue + skey = _normalize(au_m.group(1))[:5] + rid = _lookup(skey, yr_m.group(1), []) + if rid and rid not in rids: + rids.append(rid) + if not rids: + return full + return f'{full}' + + def apply(text: str) -> str: + if not text: + return text + parts = _split_re.split(text) + result = [] + for idx, part in enumerate(parts): + if idx % 2 != 0: + result.append(part) + continue + # Narrative first, then parenthetical on remaining non-xref text + part = _narrative_re.sub(_replace, part) + sub_parts = _split_re.split(part) + out = [] + for i, sp in enumerate(sub_parts): + if i % 2 != 0: + out.append(sp) + else: + out.append(_paren_inner_re.sub(_replace_paren, sp)) + result.append(''.join(out)) + return ''.join(result) + + return apply + + +def _make_hyperlink(anchor: str, text: str, template_run) -> object: + """Create a element.""" + hl = OxmlElement("w:hyperlink") + hl.set(qn("w:anchor"), anchor) + + r = _make_text_run(text, template_run) + # Ensure rPr exists and add Hyperlink style + rpr = r.find(qn("w:rPr")) + if rpr is None: + rpr = OxmlElement("w:rPr") + r.insert(0, rpr) + style_elem = OxmlElement("w:rStyle") + style_elem.set(qn("w:val"), "Hyperlink") + rpr.insert(0, style_elem) + + hl.append(r) + return hl diff --git a/users/__init__.py b/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/admin.py b/users/admin.py new file mode 100644 index 0000000..df0f4c1 --- /dev/null +++ b/users/admin.py @@ -0,0 +1,33 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from django.utils.translation import gettext_lazy as _ + +from .forms import CustomUserChangeForm, CustomUserCreationForm +from .models import CustomUser + + +class CustomUserAdmin(UserAdmin): + form = CustomUserChangeForm + add_form = CustomUserCreationForm + fieldsets = ( + (None, {"fields": ("username", "password")}), + (_("Personal info"), {"fields": ("name", "email")}), + ( + _("Permissions"), + { + "fields": ( + "is_active", + "is_staff", + "is_superuser", + "groups", + "user_permissions", + ), + }, + ), + (_("Important dates"), {"fields": ("last_login", "date_joined")}), + ) + list_display = ["username", "name", "is_superuser"] + search_fields = ["name"] + + +admin.site.register(CustomUser, CustomUserAdmin) diff --git a/users/apps.py b/users/apps.py new file mode 100644 index 0000000..72b1401 --- /dev/null +++ b/users/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'users' diff --git a/users/forms.py b/users/forms.py new file mode 100644 index 0000000..0eff734 --- /dev/null +++ b/users/forms.py @@ -0,0 +1,20 @@ +from django.contrib.auth.forms import UserChangeForm, UserCreationForm +from django.utils.translation import gettext_lazy as _ + +from .models import CustomUser + + +class CustomUserCreationForm(UserCreationForm): + class Meta: + model = CustomUser + error_messages = { + "username": {"unique": _("This username has already been taken.")} + } + fields = ('first_name', 'last_name') + + +class CustomUserChangeForm(UserChangeForm): + class Meta: + model = CustomUser + fields = ('first_name', 'last_name') + diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py new file mode 100644 index 0000000..1989842 --- /dev/null +++ b/users/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# Generated by Django 6.0.5 on 2026-06-09 00:35 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='CustomUser', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('name', models.CharField(blank=True, max_length=255, verbose_name='Name of User')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/users/migrations/__init__.py b/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/models.py b/users/models.py new file mode 100644 index 0000000..2711bd8 --- /dev/null +++ b/users/models.py @@ -0,0 +1,26 @@ +from django.contrib.auth.models import AbstractUser +from django.db import models +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + + +class CustomUser(AbstractUser): + """ + Default custom user model for SciELO Content Manager . + If adding fields that need to be filled at user signup, + check forms.SignupForm and forms.SocialSignupForms accordingly. + """ + + #: First and last name do not cover name patterns around the globe + name = models.CharField(_("Name of User"), blank=True, max_length=255) + first_name = models.CharField(max_length=150, blank=True, verbose_name="first name") + last_name = models.CharField(max_length=150, blank=True, verbose_name="last name") + + def get_absolute_url(self): + """Get url for user's detail view. + + Returns: + str: URL for user detail. + + """ + return reverse("users:detail", kwargs={"username": self.username})