diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 0244efe..f39dc2a 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,6 +1,6 @@ # These are supported funding model platforms -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +github: kiwix # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username @@ -9,4 +9,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username -custom: https://kiwix.org/support-us/ +custom: # https://kiwix.org/support-us/ diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000..08a3931 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,15 @@ +daysUntilClose: false +staleLabel: stale + +issues: + daysUntilStale: 60 + markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be now be reviewed manually. Thank you + for your contributions. +pulls: + daysUntilStale: 7 + markComment: > + This pull request has been automatically marked as stale because it has not had + recent activity. It will be now be reviewed manually. Thank you + for your contributions. diff --git a/.github/workflows/DailyTests.yaml b/.github/workflows/DailyTests.yaml new file mode 100644 index 0000000..2bc9bc5 --- /dev/null +++ b/.github/workflows/DailyTests.yaml @@ -0,0 +1,34 @@ +name: DailyTests + +on: + schedule: + - cron: "0 4 * * *" + workflow_dispatch: + + +jobs: + run-daily-tests: + runs-on: ubuntu-22.04 + + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: build zimit image + run: docker build -t local-zimit . + + - name: run crawl of test website + run: docker run -v $PWD/output:/output local-zimit zimit --seeds https://website.test.openzim.org/ --name tests_eng_test-website --zim-file tests_eng_test-website.zim + + - name: archive ZIM + uses: actions/upload-artifact@v4 + with: + name: tests_eng_test-website.zim + path: output/tests_eng_test-website.zim + retention-days: 30 + + - name: build tests-daily Docker image + run: docker build -t local-tests-daily tests-daily + + - name: run integration test suite + run: docker run -e SKIP_YOUTUBE_TEST="True" -v $PWD/tests-daily/daily.py:/app/daily.py -v $PWD/output:/output local-tests-daily bash -c "cd /app && pytest -v --log-level=INFO --log-format='%(levelname)s - %(message)s' daily.py" diff --git a/.github/workflows/Publish.yml b/.github/workflows/Publish.yml new file mode 100644 index 0000000..1ddb343 --- /dev/null +++ b/.github/workflows/Publish.yml @@ -0,0 +1,53 @@ +name: Publish released version + +on: + release: + types: [published] + +jobs: + publish-amd64: + runs-on: ubuntu-24.04 + name: "Publish for AMD64" + + steps: + - uses: actions/checkout@v4 + + - name: Build and push Docker image + uses: openzim/docker-publish-action@v10 + with: + image-name: openzim/zimit + tag-pattern: /^v([0-9.]+)$/ + latest-on-tag: true + restrict-to: openzim/zimit + registries: ghcr.io + credentials: | + GHCRIO_USERNAME=${{ secrets.GHCR_USERNAME }} + GHCRIO_TOKEN=${{ secrets.GHCR_TOKEN }} + repo_description: auto + repo_overview: auto + platforms: | + linux/amd64 + + # Disabled for now, see https://github.com/openzim/zimit/issues/463 + # publish-arm64: + # runs-on: ubuntu-24.04 + # name: "Publish for ARM64" + # + # steps: + # - uses: actions/checkout@v4 + # + # - name: Build and push Docker image + # uses: openzim/docker-publish-action@v10 + # with: + # image-name: openzim/zimit + # tag-pattern: /^v([0-9.]+)$/ + # latest-on-tag: true + # restrict-to: openzim/zimit + # registries: ghcr.io + # credentials: | + # GHCRIO_USERNAME=${{ secrets.GHCR_USERNAME }} + # GHCRIO_TOKEN=${{ secrets.GHCR_TOKEN }} + # repo_description: auto + # repo_overview: auto + # platforms: | + # linux/arm64 diff --git a/.github/workflows/PublishDockerDevImage.yaml b/.github/workflows/PublishDockerDevImage.yaml new file mode 100644 index 0000000..1cbecea --- /dev/null +++ b/.github/workflows/PublishDockerDevImage.yaml @@ -0,0 +1,55 @@ +name: Publish Docker dev image + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + publish-amd64: + runs-on: ubuntu-24.04 + name: "Publish for AMD64" + + steps: + - uses: actions/checkout@v4 + + - name: Build and push Docker image + uses: openzim/docker-publish-action@v10 + with: + image-name: openzim/zimit + manual-tag: dev + latest-on-tag: false + restrict-to: openzim/zimit + registries: ghcr.io + credentials: | + GHCRIO_USERNAME=${{ secrets.GHCR_USERNAME }} + GHCRIO_TOKEN=${{ secrets.GHCR_TOKEN }} + repo_description: auto + repo_overview: auto + platforms: | + linux/amd64 + + # Disabled for now, see https://github.com/openzim/zimit/issues/463 + # publish-arm64: + # runs-on: ubuntu-24.04-arm + # name: "Publish for ARM64" + # + # steps: + # - uses: actions/checkout@v4 + # + # - name: Build and push Docker image + # uses: openzim/docker-publish-action@v10 + # with: + # image-name: openzim/zimit + # manual-tag: dev + # latest-on-tag: false + # restrict-to: openzim/zimit + # registries: ghcr.io + # credentials: | + # GHCRIO_USERNAME=${{ secrets.GHCR_USERNAME }} + # GHCRIO_TOKEN=${{ secrets.GHCR_TOKEN }} + # repo_description: auto + # repo_overview: auto + # platforms: | + # linux/arm64 diff --git a/.github/workflows/QA.yaml b/.github/workflows/QA.yaml new file mode 100644 index 0000000..9cd2fea --- /dev/null +++ b/.github/workflows/QA.yaml @@ -0,0 +1,34 @@ +name: QA + +on: + pull_request: + push: + branches: + - main + +jobs: + check-qa: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: pyproject.toml + architecture: x64 + + - name: Install dependencies (and project) + run: | + pip install -U pip + pip install -e .[lint,scripts,test,check] + + - name: Check black formatting + run: inv lint-black + + - name: Check ruff + run: inv lint-ruff + + - name: Check pyright + run: inv check-pyright diff --git a/.github/workflows/Tests.yaml b/.github/workflows/Tests.yaml new file mode 100644 index 0000000..8c74b21 --- /dev/null +++ b/.github/workflows/Tests.yaml @@ -0,0 +1,81 @@ +name: Tests + +on: + pull_request: + push: + branches: + - main + +jobs: + run-tests: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: pyproject.toml + architecture: x64 + + - name: Install dependencies (and project) + run: | + pip install -U pip + pip install -e .[test,scripts] + + - name: Run the tests + run: inv coverage --args "-vvv" + + - name: Upload coverage report to codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + + build_python: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version-file: pyproject.toml + architecture: x64 + + - name: Ensure we can build Python targets + run: | + pip install -U pip build + python3 -m build --sdist --wheel + + # this job replaces the standard "build_docker" job since it builds the docker image + run-integration-tests: + runs-on: ubuntu-22.04 + + steps: + - name: checkout + uses: actions/checkout@v4 + + - name: build image + run: docker build -t local-zimit . + + - name: ensure help display without issue + run: docker run -v $PWD/output:/output local-zimit zimit --help + + - name: run crawl with soft size limit + run: docker run -v $PWD/output:/output local-zimit zimit --seeds http://website.test.openzim.org/ --sizeSoftLimit 8192 --name tests_en_sizesoftlimit --zim-file tests_en_sizesoftlimit.zim --adminEmail test@example.com --mobileDevice "Pixel 5" --zimit-progress-file /output/stats_sizesoftlimit.json + + - name: run crawl with hard size limit + run: docker run -v $PWD/output:/output local-zimit zimit --seeds http://website.test.openzim.org/ --sizeHardLimit 8192 --name tests_en_sizehardlimit --zim-file tests_en_sizehardlimit.zim --adminEmail test@example.com --mobileDevice "Pixel 5" --zimit-progress-file /output/stats_sizehardlimit.json || true + + - name: run crawl with soft time limit + run: docker run -v $PWD/output:/output local-zimit zimit --seeds http://website.test.openzim.org/ --timeSoftLimit 1 --name tests_en_timesoftlimit --zim-file tests_en_timesoftlimit.zim --adminEmail test@example.com --mobileDevice "Pixel 5" --zimit-progress-file /output/stats_timesoftlimit.json + + - name: run crawl with hard time limit + run: docker run -v $PWD/output:/output local-zimit zimit --seeds http://website.test.openzim.org/ --timeHardLimit 1 --name tests_en_timehardlimit --zim-file tests_en_timehardlimit.zim --adminEmail test@example.com --mobileDevice "Pixel 5" --zimit-progress-file /output/stats_timehardlimit.json || true + + - name: run standard crawl + run: docker run -v $PWD/output:/output local-zimit zimit --seeds http://website.test.openzim.org/http-return-codes.html --name tests_en_onepage --zim-file tests_en_onepage.zim --adminEmail test@example.com --mobileDevice "Pixel 5" --zimit-progress-file /output/stats.json --statsFilename /output/crawl.json --warc2zim-progress-file /output/warc2zim.json --keep + + - name: run integration test suite + run: docker run -v $PWD/tests-integration/integration.py:/app/integration.py -v $PWD/output:/output local-zimit bash -c "/app/zimit/bin/pip install pytest; /app/zimit/bin/pytest -v /app/integration.py" diff --git a/.github/workflows/update-zim-offliner-definition.yaml b/.github/workflows/update-zim-offliner-definition.yaml new file mode 100644 index 0000000..f481354 --- /dev/null +++ b/.github/workflows/update-zim-offliner-definition.yaml @@ -0,0 +1,45 @@ +name: Update ZIMFarm Definitions + +on: + push: + branches: [main] + paths: + - "offliner-definition.json" + release: + types: [published] + + workflow_dispatch: + inputs: + version: + description: "Version to publish" + required: false + default: "dev" + +jobs: + prepare-json: + runs-on: ubuntu-24.04 + outputs: + offliner_definition_b64: ${{ steps.read-json.outputs.offliner_definition_b64 }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - id: read-json + run: | + if [ ! -f "offliner-definition.json" ]; then + echo "File not found!" >&2 + exit 1 + fi + json_b64=$(base64 -w0 <<< "$(jq -c . offliner-definition.json)") + echo "offliner_definition_b64=$json_b64" >> $GITHUB_OUTPUT + call-workflow: + needs: prepare-json + uses: openzim/overview/.github/workflows/update-zimfarm-offliner-definition.yaml@main + with: + version: ${{ github.event_name == 'release' && github.event.release.tag_name || (github.event.inputs.version || 'dev') }} + offliner: zimit + offliner_definition_b64: ${{ needs.prepare-json.outputs.offliner_definition_b64 }} + secrets: + zimfarm_ci_secret: ${{ secrets.ZIMFARM_CI_SECRET }} diff --git a/.gitignore b/.gitignore index 22e9769..0a2d877 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,362 @@ -*.pyc -__pycache__ -*.zim +# Created by https://www.toptal.com/developers/gitignore/api/linux,macos,python,visualstudiocode,intellij +# Edit at https://www.toptal.com/developers/gitignore?templates=linux,macos,python,visualstudiocode,intellij + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$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 + +# 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 + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# 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/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# End of https://www.toptal.com/developers/gitignore/api/linux,macos,python,visualstudiocode,intellij + +# output dir +output + +# ignore all vscode, this editor specific, not maintained by openzim +.vscode diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b362d62 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer +- repo: https://github.com/psf/black + rev: "25.1.0" + hooks: + - id: black +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.4 + hooks: + - id: ruff +- repo: https://github.com/RobertCraigie/pyright-python + rev: v1.1.393 + hooks: + - id: pyright + name: pyright (system) + description: 'pyright static type checker' + entry: pyright + language: system + 'types_or': [python, pyi] + require_serial: true + minimum_pre_commit_version: '2.9.2' diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..2a99b30 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,409 @@ +## Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) (as of version 1.2.0). + +## [Unreleased] + +### Added +- Added `--overwrite` flag to overwrite existing ZIM file if it exists (#399) + +### Changed +- Fix issues preventing interrupted crawls from being resumed. (#499) + - Ensure build directory is used explicitly instead of a randomized subdirectory when passed, and pre-create it if it does not exist. + - Use all warc_dirs found instead of just the latest so interrupted crawls use all collected pages across runs when an explicit collections directory is not passed. + - Don't cleanup an explicitly passed build directory. + +## [3.0.5] - 2024-04-11 + +### Changed + +- Upgrade to browsertrix crawler 1.6.0 (#493) + +## [3.0.4] - 2024-04-04 + +### Changed + +- Upgrade to browsertrix crawler 1.5.10 (#491) + +## [3.0.3] - 2024-02-28 + +### Changed + +- Upgrade to browsertrix crawler 1.5.7 (#483) + +## [3.0.2] - 2024-02-27 + +### Changed + +- Upgrade to browsertrix crawler 1.5.6 (#482) + +## [3.0.1] - 2024-02-24 + +### Changed + +- Upgrade to browsertrix crawler 1.5.4 (#476) + +## [3.0.0] - 2024-02-17 + +### Changed + +- Change solution to report partial ZIM to the Zimfarm and other clients (#304) +- Keep temporary folder when crawler or warc2zim fails, even if not asked for (#468) +- Add many missing Browsertrix Crawler arguments ; drop default overrides by zimit ; drop `--noMobileDevice` setting (not needed anymore) (#433) +- Document all Browsertrix Crawler default arguments values (#416) +- Use preferred Browsertrix Crawler arguments names: (part of #471) + - `--seeds` instead of `--url` + - `--seedFile` instead of `--urlFile` + - `--pageLimit` instead of `--limit` + - `--pageLoadTimeout` instead of `--timeout` + - `--scopeIncludeRx` instead of `--include` + - `--scopeExcludeRx` instead of `--exclude` + - `--pageExtraDelay` instead of `--delay` +- Remove confusion between zimit, warc2zim and crawler stats filenames (part of #471) + - `--statsFilename` is now the crawler stats file (since it is the same name, just like other arguments) + - `--zimit-progress-file` is now the zimit stats location + - `--warc2zim-progress-file` is the warc2zim stats location + - all are optional values, if not set and needed temporary files are used + +### Fixed + +- Do not create the ZIM when crawl is incomplete (#444) + +## [2.1.8] - 2024-02-07 + +### Changed + +- Upgrade to browsertrix crawler 1.5.1, Python 3.13 and others (#462 + #464) + +## [2.1.7] - 2024-01-10 + +### Changed + +- Upgrade to browsertrix crawler 1.4.2 (#450) +- Upgrade to warc2zim 2.2.0 + +## [2.1.6] - 2024-11-07 + +### Changed + +- Upgrade to browsertrix crawler 1.3.5 (#426) + +## [2.1.5] - 2024-11-01 + +### Changed + +- Upgrade to browsertrix crawler 1.3.4 and warc2zim 2.1.3 (#424) + +## [2.1.4] - 2024-10-11 + +### Changed + +- Upgrade to browsertrix crawler 1.3.3 (#411) + +## [2.1.3] - 2024-10-08 + +### Changed + +- Upgrade to browsertrix crawler 1.3.2, warc2zim 2.1.2 and other dependencies (#406) + +### Fixed + +- Fix help (#393) + +## [2.1.2] - 2024-09-09 + +### Changed + +- Upgrade to browsertrix crawler 1.3.0-beta.1 (#387) (fixes "Ziming a website with huge assets (e.g. PDFs) is failing to proceed" - #380) + +## [2.1.1] - 2024-09-05 + +### Added + +- Add support for uncompressed tar archive in --warcs (#369) + +### Changed + +- Upgrade to browsertrix crawler 1.3.0-beta.0 (#379), including upgrage to Ubuntu Noble (#307) + +### Fixed + +- Stream files downloads to not exhaust memory (#373) +- Fix documentation on `--diskUtilization` setting (#375) + +## [2.1.0] - 2024-08-09 + +### Added + +- Add `--custom-behaviors` argument to support path/HTTP(S) URL custom behaviors to pass to the crawler (#313) +- Add daily automated end-to-end tests of a page with Youtube player (#330) +- Add `--warcs` option to directly process WARC files (#301) + +### Changed + +- Make it clear that `--profile` argument can be an HTTP(S) URL (and not only a path) (#288) +- Fix README imprecisions + add back warc2zim availability in docker image (#314) +- Enhance integration test to assert final content of the ZIM (#287) +- Stop fetching and passing browsertrix crawler version as scraperSuffix to warc2zim (#354) +- Do not log number of WARC files found (#357) +- Upgrade dependencies (warc2zim 2.1.0) + +### Fixed + +- Sort WARC directories found by modification time (#366) + +## [2.0.6] - 2024-08-02 + +### Changed + +- Upgraded Browsertrix Crawler to 1.2.6 + +## [2.0.5] - 2024-07-24 + +### Changed + +- Upgraded Browsertrix Crawler to 1.2.5 +- Upgraded warc2zim to 2.0.3 + +## [2.0.4] - 2024-07-15 + +### Changed + +- Upgraded Browsertrix Crawler to 1.2.4 (fixes retrieve automatically the assets present in a data-xxx tag #316) + +## [2.0.3] - 2024-06-24 + +### Changed + +- Upgraded Browsertrix Crawler to 1.2.0 (fixes Youtube videos issue #323) + +## [2.0.2] - 2024-06-18 + +### Changed + +- Upgrade dependencies (mainly warc2zim 2.0.2) + + +## [2.0.1] - 2024-06-13 + +### Changed + +- Upgrade dependencies (especially warc2zim 2.0.1 and browsertrix crawler 1.2.0-beta.0) (#318) + +### Fixed + +- Crawler is not correctly checking disk size / usage (#305) + +## [2.0.0] - 2024-06-04 + +### Added + +- New `--version` flag to display Zimit version (#234) +- New `--logging` flag to adjust Browsertrix Crawler logging (#273) +- Use new `--scraper-suffix` flag of warc2zim to enhance ZIM "Scraper" metadata (#275) +- New `--noMobileDevice` CLI argument +- Publish Docker image for `linux/arm64` (in addition to `linux/amd64`) (#178) + +### Changed + +- **Use `warc2zim` version 2**, which works without Service Worker anymore (#193) +- Upgraded Browsertrix Crawler to 1.1.3 +- Adopt Python bootstrap conventions +- Upgrade to Python 3.12 + upgrade dependencies +- Removed handling of redirects by zimit, they are handled by browsertrix crawler and detected properly by warc2zim (#284) +- Drop initial check of URL in Python (#256) +- `--userAgent` CLI argument overrides again the `--userAgentSuffix` and `--adminEmail` values +- `--userAgent` CLI arguement is not mandatory anymore + +### Fixed + +- Fix support for Youtube videos (#291) +- Fix crawler `--waitUntil` values (#289) + +## [1.6.3] - 2024-01-18 + +### Changed + +- Adapt to new `warc2zim` code structure +- Using browsertrix-crawler 0.12.4 +- Using warc2zim 1.5.5 + +### Added + +- New `--build` parameter (optional) to specify the directory holding Browsertrix files ; if not set, `--output` +directory is used ; zimit creates one subdir of this folder per invocation to isolate datasets ; subdir is kept only +if `--keep` is set. + +### Fixed + +- `--collection` parameter was not working (#252) + +## [1.6.2] - 2023-11-17 + +### Changed + +- Using browsertrix-crawler 0.12.3 + +### Fixed + +- Fix logic passing args to crawler to support value '0' (#245) +- Fix documentation about Chrome and headless (#248) + +## [1.6.1] - 2023-11-06 + +### Changed + +- Using browsertrix-crawler 0.12.1 + +## [1.6.0] - 2023-11-02 + +### Changed + +- Scraper fails for all HTTP error codes returned when checking URL at startup (#223) +- User-Agent now has a default value (#228) +- Manipulation of spaces with UA suffix and adminEmail has been modified +- Same User-Agent is used for check_url (Python) and Browsertrix crawler (#227) +- Using browsertrix-crawler 0.12.0 + +## [1.5.3] - 2023-10-02 + +### Changed + +- Using browsertrix-crawler 0.11.2 + +## [1.5.2] - 2023-09-19 + +### Changed + +- Using browsertrix-crawler 0.11.1 + +## [1.5.1] - 2023-09-18 + +### Changed + +- Using browsertrix-crawler 0.11.0 +- Scraper stat file is not created empty (#211) +- Crawler statistics are not available anymore (#213) +- Using warc2zim 1.5.4 + +## [1.5.0] - 2023-08-23 + +### Added + +- `--long-description` param + +## [1.4.1] - 2023-08-23 + +### Changed + +- Using browsertrix-crawler 0.10.4 +- Using warc2zim 1.5.3 + +## [1.4.0] - 2023-08-02 + +### Added + +- `--title` to set ZIM title +- `--description` to set ZIM description +- New crawler options: `--maxPageLimit`, `--delay`, `--diskUtilization` +- `--zim-lang` param to set warc2zim's `--lang` (ISO-639-3) + +### Changed + +- Using browsertrix-crawler 0.10.2 +- Default and accepted values for `--waitUntil` from crawler's update +- Using warc2zim 1.5.2 +- Disabled Chrome updates to prevent incidental inclusion of update data in WARC/ZIM (#172) +- `--failOnFailedSeed` used inconditionally +- `--lang` now passed to crawler (ISO-639-1) + +### Removed + +- `--newContext` from crawler's update + +## [1.3.1] - 2023-02-06 + +### Changed + +- Using browsertrix-crawler 0.8.0 +- Using warc2zim version 1.5.1 with wabac.js 2.15.2 + +## [1.3.0] - 2023-02-02 + +### Added + +- Initial url check normalizes homepage redirects to standart ports – 80/443 (#137) + +### Changed + +- Using warc2zim version 1.5.0 with scope conflict fix and videos fix +- Using browsertrix-crawler 0.8.0-beta.1 +- Fixed `--allowHashUrls` being a boolean param +- Increased `check_url` timeout (12s to connect, 27s to read) instead of 10s + +## [1.2.0] - 2022-06-21 + +### Added + +- `--urlFile` browsertrix crawler parameter +- `--depth` browsertrix crawler parameter +- `--extraHops`, parameter +- `--collection` browsertrix crawler parameter +- `--allowHashUrls` browsertrix crawler parameter +- `--userAgentSuffix` browsertrix crawler parameter +- `--behaviors`, parameter +- `--behaviorTimeout` browsertrix crawler parameter +- `--profile` browsertrix crawler parameter +- `--sizeLimit` browsertrix crawler parameter +- `--timeLimit` browsertrix crawler parameter +- `--healthCheckPort`, parameter +- `--overwrite` parameter + +### Changed + +- using browsertrix-crawler `0.6.0` and warc2zim `1.4.2` +- default WARC location after crawl changed +from `collections/capture-*/archive/` to `collections/crawl-*/archive/` + +### Removed + +- `--scroll` browsertrix crawler parameter (see `--behaviors`) +- `--scope` browsertrix crawler parameter (see `--scopeType`, `--include` and `--exclude`) + + +## [1.1.5] + +- using crawler 0.3.2 and warc2zim 1.3.6 + +## [1.1.4] + +- Defaults to `load,networkidle0` for waitUntil param (same as crawler) +- Allows setting combinations of values for waitUntil param +- Updated warc2zim to 1.3.5 +- Updated browsertrix-crawler to 0.3.1 +- Warc to zim now written to `{temp_root_dir}/collections/capture-*/archive/` where + `capture-*` is dynamic and includes the datetime. (from browsertrix-crawler) + +## [1.1.3] + +- allows same first-level-domain redirects +- fixed redirects to URL in scope +- updated crawler to 0.2.0 +- `statsFilename` now informs whether limit was hit or not + +## [1.1.2] + +- added support for --custom-css +- added domains block list (dfault) + +## [1.1.1] + +- updated browsertrix-crawler to 0.1.4 + - autofetcher script to be injected by defaultDriver to capture srcsets + URLs in dynamically added stylesheets + +## [1.0] + +- initial version using browsertrix-crawler:0.1.3 and warc2zim:1.3.3 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9666c0b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,49 @@ +FROM webrecorder/browsertrix-crawler:1.6.0 +LABEL org.opencontainers.image.source=https://github.com/openzim/zimit + +# add deadsnakes ppa for latest Python on Ubuntu +RUN add-apt-repository ppa:deadsnakes/ppa -y + +RUN apt-get update \ + && apt-get install -qqy --no-install-recommends \ + libmagic1 \ + python3.13-venv \ + && rm -rf /var/lib/apt/lists/* \ + # python setup (in venv not to conflict with browsertrix) + && python3.13 -m venv /app/zimit \ + # placeholder (default output location) + && mkdir -p /output \ + # disable chrome upgrade + && printf "repo_add_once=\"false\"\nrepo_reenable_on_distupgrade=\"false\"\n" > /etc/default/google-chrome \ + # download list of bad domains to filter-out. intentionnaly ran post-install \ + # so it's not cached in earlier layers (url stays same but content updated) \ + && mkdir -p /tmp/ads \ + && cd /tmp/ads \ + && curl -L -O https://hosts.anudeep.me/mirror/adservers.txt \ + && curl -L -O https://hosts.anudeep.me/mirror/CoinMiner.txt \ + && curl -L -O https://hosts.anudeep.me/mirror/facebook.txt \ + && cat ./*.txt > /etc/blocklist.txt \ + && rm ./*.txt \ + && printf '#!/bin/sh\ncat /etc/blocklist.txt >> /etc/hosts\nexec "$@"' > /usr/local/bin/entrypoint.sh \ + && chmod +x /usr/local/bin/entrypoint.sh + +# Copy pyproject.toml and its dependencies +COPY pyproject.toml README.md /src/ +COPY src/zimit/__about__.py /src/src/zimit/__about__.py + +# Install Python dependencies +RUN . /app/zimit/bin/activate && python -m pip install --no-cache-dir /src + +# Copy code + associated artifacts +COPY src /src/src +COPY *.md /src/ + +# Install + cleanup +RUN . /app/zimit/bin/activate && python -m pip install --no-cache-dir /src \ + && ln -s /app/zimit/bin/zimit /usr/bin/zimit \ + && ln -s /app/zimit/bin/warc2zim /usr/bin/warc2zim \ + && chmod +x /usr/bin/zimit \ + && rm -rf /src + +ENTRYPOINT ["entrypoint.sh"] +CMD ["zimit", "--help"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..188615f --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +Zimit +===== + +Zimit is a scraper allowing to create [ZIM file](https://en.wikipedia.org/wiki/ZIM_(file_format)) from any Web site. + +[![CodeFactor](https://www.codefactor.io/repository/github/openzim/zimit/badge)](https://www.codefactor.io/repository/github/openzim/zimit) +[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) +[![Docker](https://ghcr-badge.egpl.dev/openzim/zimit/latest_tag?label=docker)](https://ghcr.io/openzim/zimit) + +Zimit adheres to openZIM's [Contribution Guidelines](https://github.com/openzim/overview/wiki/Contributing). + +Zimit has implemented openZIM's [Python bootstrap, conventions and policies](https://github.com/openzim/_python-bootstrap/blob/main/docs/Policy.md) **v1.0.1**. + +Capabilities and known limitations +-------------------- + +While we would like to support as many websites as possible, making an offline archive of any website with a versatile tool obviously has some limitations. + +Most capabilities and known limitations are documented in [warc2zim README](https://github.com/openzim/warc2zim/blob/main/README.md). There are also some limitations in Browsertrix Crawler (used to fetch the website) and wombat (used to properly replay dynamic web requests), but these are not (yet?) clearly documented. + +Technical background +-------------------- + +Zimit runs a fully automated browser-based crawl of a website property and produces a ZIM of the crawled content. Zimit runs in a Docker container. + +The system: +- runs a website crawl with [Browsertrix Crawler](https://github.com/webrecorder/browsertrix-crawler), which produces WARC files +- converts the crawled WARC files to a single ZIM using [warc2zim](https://github.com/openzim/warc2zim) + +The `zimit.py` is the entrypoint for the system. + +After the crawl is done, warc2zim is used to write a zim to the `/output` directory, which should be mounted as a volume to not loose the ZIM created when container stops. + +Using the `--keep` flag, the crawled WARCs and few other artifacts will also be kept in a temp directory inside `/output` + +Usage +----- + +`zimit` is intended to be run in Docker. Docker image is published at https://github.com/orgs/openzim/packages/container/package/zimit. + +The image accepts the following parameters, **as well as any of the [Browsertrix crawler](https://crawler.docs.browsertrix.com/user-guide/cli-options/) and [warc2zim](https://github.com/openzim/warc2zim) ones**: + +- Required: `--seeds URL` - the url to start crawling from ; multiple URLs can be separated by a comma (even if **usually not needed**, these are just the **seeds** of the crawl) ; first seed URL is used as ZIM homepage +- Required: `--name` - Name of ZIM file +- `--output` - output directory (defaults to `/output`) +- `--pageLimit U` - Limit capture to at most U URLs +- `--scopeExcludeRx ` - skip URLs that match the regex from crawling. Can be specified multiple times. An example is `--scopeExcludeRx="(\?q=|signup-landing\?|\?cid=)"`, where URLs that contain either `?q=` or `signup-landing?` or `?cid=` will be excluded. +- `--workers N` - number of crawl workers to be run in parallel +- `--waitUntil` - Puppeteer setting for how long to wait for page load. See [page.goto waitUntil options](https://github.com/puppeteer/puppeteer/blob/main/docs/api.md#pagegotourl-options). The default is `load`, but for static sites, `--waitUntil domcontentloaded` may be used to speed up the crawl (to avoid waiting for ads to load for example). +- `--keep` - in case of failure, WARC files and other temporary files (which are stored as a subfolder of output directory) are always kept, otherwise they are automatically deleted. Use this flag to always keep WARC files, even in case of success. + +Example command: + +```bash +docker run ghcr.io/openzim/zimit zimit --help +docker run ghcr.io/openzim/zimit warc2zim --help +docker run -v /output:/output ghcr.io/openzim/zimit zimit --seeds URL --name myzimfile +``` + +**Note**: Image automatically filters out a large number of ads by using the 3 blocklists from [anudeepND](https://github.com/anudeepND/blacklist). If you don't want this filtering, disable the image's entrypoint in your container (`docker run --entrypoint="" ghcr.io/openzim/zimit ...`). + +To re-build the Docker image locally run: + +```bash +docker build -t ghcr.io/openzim/zimit . +``` + +FAQ +--- + +The Zimit contributor's team maintains [a page with most Frequently Asked Questions](https://github.com/openzim/zimit/wiki/Frequently-Asked-Questions). + +Nota bene +--------- + +While Zimit 1.x relied on a Service Worker to display the ZIM content, this is not anymore the case +since Zimit 2.x which does not have any special requirements anymore. + +It should also be noted that a first version of a generic HTTP scraper was created in 2016 during +the [Wikimania Esino Lario +Hackathon](https://wikimania2016.wikimedia.org/wiki/Programme/Kiwix-dedicated_Hackathon). + +That version is now considered outdated and [archived in `2016` +branch](https://github.com/openzim/zimit/tree/2016). + +License +------- + +[GPLv3](https://www.gnu.org/licenses/gpl-3.0) or later, see +[LICENSE](LICENSE) for more details. diff --git a/README.rst b/README.rst deleted file mode 100644 index 12babe2..0000000 --- a/README.rst +++ /dev/null @@ -1,246 +0,0 @@ -##################################### -Create ZIM files out of HTTP websites -##################################### - -This project provides an API and an user interface in order to convert any -website into a Zim file. - -Exposed API -########### - -All APIs are talking JSON over HTTP. As such, all parameters should be sent as -stringified JSON and the Content-Type should be set to "application/json". - -POST /website-zim -================= - -By posting to this endpoint, you are asking the system to start a new download -of a website and a conversion into a Zim format. - -Required parameters -------------------- - -- **url**: URL of the website to be crawled -- **title**: Title that will be used in the created Zim file -- **email**: Email address that will get notified when the creation of the file is over - -Optional parameters -------------------- - -- **language**: An `ISO 639-3 `_ code - representing the language -- **welcome**: the page that will be first shown in the Zim file -- **description**: The description that will be embedded in the Zim file -- **author**: The author of the content - -Return values -------------- - -- **job_id**: The job id is returned in JSON format. It can be used to know the - status of the process. - -Status codes ------------- - -- `400 Bad Request` will be returned in case you are not respecting the - expected inputs. In case of error, have a look at the body of the response: - it contains information about what is missing. -- `201 Created` will be returned if the process started. - -Exemple -------- - -:: - - $ http POST http://0.0.0.0:6543/website-url url="https://refugeeinfo.eu/" title="Refugee Info" email="alexis@notmyidea.org" - HTTP/1.1 201 Created - - { - "job": "5012abe3-bee2-4dd7-be87-39a88d76035d" - } - - -GET /status/{jobid} -=================== - -Retrieve the status of a job and displays the associated logs. - -Return values -------------- - -- **status**: The status of the job, it is one of 'queued', finished', - 'failed', 'started' and 'deferred'. -- **log**: The logs of the job. - -Status codes ------------- - -- `404 Not Found` will be returned in case the requested job does not exist. -- `200 OK` will be returned in any other case. - -Exemple -------- - -:: - - http GET http://0.0.0.0:6543/status/5012abe3-bee2-4dd7-be87-39a88d76035d - HTTP/1.1 200 OK - - { - "log": "", - "status": "finished" - } - - -Okay, so how do I install it on my server? -########################################## - -Currently, the best way to install it is by retrieving the sources from github - -:: - - $ git clone https://github.com/almet/zimit.git - $ cd zimit - -Create a virtual environment and install the project in it:: - - $ virtualenv venv - $ venv/bin/pip install -e . - -Then, run it how you want, for instance with pserve:: - - $ venv/bin/pserve zimit.ini - - -In a separate process, you also need to run the worker:: - - $ venv/bin/rqworker - - -And you're ready to go. To test it:: - - $ http POST http://0.0.0.0:6543/website-url url="https://refugeeinfo.eu/" title="Refugee Info" email="alexis@notmyidea.org" - - -Debian dependencies -#################### - -Installing the dependencies -=========================== - -:: - - sudo apt-get install httrack libzim-dev libmagic-dev liblzma-dev libz-dev build-essential libtool libgumbo-dev redis-server automake pkg-config - -Installing zimwriterfs -====================== - -:: - - git clone https://github.com/wikimedia/openzim.git - cd openzim/zimwriterfs - ./autogen.sh - ./configure - make - -Then upgrade the path to zimwriterfs executable in zimit.ini - -:: - - $ rqworker & pserve zimit.ini - -How to deploy? -############## - -There are multiple ways to deploy such service, so I'll describe how I do it -with my own best-practices. - -First of all, get all the dependencies and the code. I like to have everything -available in /home/www, so let's consider this will be the case here:: - - $ mkdir /home/www/zimit.notmyidea.org - $ cd /home/www/zimit.notmyidea.org - $ git clone https://github.com/almet/zimit.git - -Then, you can change the configuration file, by creating a new one:: - - $ cd zimit - $ cp zimit.ini local.ini - -From there, you need to update the configuration to point to the correct -binaries and locations. - -Nginx configuration -=================== - -:: - - # the upstream component nginx needs to connect to - upstream zimit_upstream { - server unix:///tmp/zimit.sock; - } - - # configuration of the server - server { - listen 80; - listen [::]:80; - server_name zimit.ideascube.org; - charset utf-8; - - client_max_body_size 200M; - - location /zims { - alias /home/ideascube/zimit.ideascube.org/zims/; - autoindex on; - } - - # Finally, send all non-media requests to the Pyramid server. - location / { - uwsgi_pass zimit_upstream; - include /var/ideascube/uwsgi_params; - } - } - - -UWSGI configuration -=================== - -:: - - [uwsgi] - uid = ideascube - gid = ideascube - chdir = /home/ideascube/zimit.ideascube.org/zimit/ - ini = /home/ideascube/zimit.ideascube.org/zimit/local.ini - # the virtualenv (full path) - home = /home/ideascube/zimit.ideascube.org/venv/ - - # process-related settings - # master - master = true - # maximum number of worker processes - processes = 4 - # the socket (use the full path to be safe - socket = /tmp/zimit.sock - # ... with appropriate permissions - may be needed - chmod-socket = 666 - # stats = /tmp/ideascube.stats.sock - # clear environment on exit - vacuum = true - plugins = python - - -supervisord configuration -========================= - -:: - - [program:zimit-worker] - command=/home/ideascube/zimit.ideascube.org/venv/bin/rqworker - directory=/home/ideascube/zimit.ideascube.org/zimit/ - user=www-data - autostart=true - autorestart=true - redirect_stderr=true - -That's it! diff --git a/app.wsgi b/app.wsgi deleted file mode 100644 index f66d9b8..0000000 --- a/app.wsgi +++ /dev/null @@ -1,24 +0,0 @@ -try: - import ConfigParser as configparser -except ImportError: - import configparser -import logging.config -import os - -from zimit import main - -here = os.path.dirname(__file__) - -ini_path = os.environ.get('ZIMIT_INI') -if ini_path is None: - ini_path = os.path.join(here, 'local.ini') - -# Set up logging -logging.config.fileConfig(ini_path) - -# Parse config and create WSGI app -config = configparser.ConfigParser() -config.read(ini_path) - -application = main(config.items('DEFAULT'), **dict(config.items('app:main' -))) diff --git a/app/assets/alertify.css b/app/assets/alertify.css deleted file mode 100644 index a49a7e6..0000000 --- a/app/assets/alertify.css +++ /dev/null @@ -1 +0,0 @@ -.alertify-logs>*{padding:12px 24px;color:#fff;box-shadow:0 2px 5px 0 rgba(0,0,0,.2);border-radius:1px}.alertify-logs>*,.alertify-logs>.default{background:rgba(0,0,0,.8)}.alertify-logs>.error{background:rgba(244,67,54,.8)}.alertify-logs>.success{background:rgba(76,175,80,.9)}.alertify{position:fixed;background-color:rgba(0,0,0,.3);left:0;right:0;top:0;bottom:0;width:100%;height:100%;z-index:1}.alertify.hide{opacity:0;pointer-events:none}.alertify,.alertify.show{box-sizing:border-box;transition:all .33s cubic-bezier(.25,.8,.25,1)}.alertify,.alertify *{box-sizing:border-box}.alertify .dialog{padding:12px}.alertify .alert,.alertify .dialog{width:100%;margin:0 auto;position:relative;top:50%;transform:translateY(-50%)}.alertify .alert>*,.alertify .dialog>*{width:400px;max-width:95%;margin:0 auto;text-align:center;padding:12px;background:#fff;box-shadow:0 2px 4px -1px rgba(0,0,0,.14),0 4px 5px 0 rgba(0,0,0,.098),0 1px 10px 0 rgba(0,0,0,.084)}.alertify .alert .msg,.alertify .dialog .msg{padding:12px;margin-bottom:12px;margin:0;text-align:left}.alertify .alert input:not(.form-control),.alertify .dialog input:not(.form-control){margin-bottom:15px;width:100%;font-size:100%;padding:12px}.alertify .alert input:not(.form-control):focus,.alertify .dialog input:not(.form-control):focus{outline-offset:-2px}.alertify .alert nav,.alertify .dialog nav{text-align:right}.alertify .alert nav button:not(.btn):not(.pure-button):not(.md-button):not(.mdl-button),.alertify .dialog nav button:not(.btn):not(.pure-button):not(.md-button):not(.mdl-button){background:transparent;box-sizing:border-box;color:rgba(0,0,0,.87);position:relative;outline:0;border:0;display:inline-block;-ms-flex-align:center;-ms-grid-row-align:center;align-items:center;padding:0 6px;margin:6px 8px;line-height:36px;min-height:36px;white-space:nowrap;min-width:88px;text-align:center;text-transform:uppercase;font-size:14px;text-decoration:none;cursor:pointer;border:1px solid transparent;border-radius:2px}.alertify .alert nav button:not(.btn):not(.pure-button):not(.md-button):not(.mdl-button):active,.alertify .alert nav button:not(.btn):not(.pure-button):not(.md-button):not(.mdl-button):hover,.alertify .dialog nav button:not(.btn):not(.pure-button):not(.md-button):not(.mdl-button):active,.alertify .dialog nav button:not(.btn):not(.pure-button):not(.md-button):not(.mdl-button):hover{background-color:rgba(0,0,0,.05)}.alertify .alert nav button:not(.btn):not(.pure-button):not(.md-button):not(.mdl-button):focus,.alertify .dialog nav button:not(.btn):not(.pure-button):not(.md-button):not(.mdl-button):focus{border:1px solid rgba(0,0,0,.1)}.alertify .alert nav button.btn,.alertify .dialog nav button.btn{margin:6px 4px}.alertify-logs{position:fixed;z-index:1}.alertify-logs.bottom,.alertify-logs:not(.top){bottom:16px}.alertify-logs.left,.alertify-logs:not(.right){left:16px}.alertify-logs.left>*,.alertify-logs:not(.right)>*{float:left;transform:translateZ(0);height:auto}.alertify-logs.left>.show,.alertify-logs:not(.right)>.show{left:0}.alertify-logs.left>*,.alertify-logs.left>.hide,.alertify-logs:not(.right)>*,.alertify-logs:not(.right)>.hide{left:-110%}.alertify-logs.right{right:16px}.alertify-logs.right>*{float:right;transform:translateZ(0)}.alertify-logs.right>.show{right:0;opacity:1}.alertify-logs.right>*,.alertify-logs.right>.hide{right:-110%;opacity:0}.alertify-logs.top{top:0}.alertify-logs>*{box-sizing:border-box;transition:all .4s cubic-bezier(.25,.8,.25,1);position:relative;clear:both;backface-visibility:hidden;perspective:1000;max-height:0;margin:0;padding:0;overflow:hidden;opacity:0;pointer-events:none}.alertify-logs>.show{margin-top:12px;opacity:1;max-height:1000px;padding:12px;pointer-events:auto} \ No newline at end of file diff --git a/app/assets/alertify.js b/app/assets/alertify.js deleted file mode 100644 index bbd9136..0000000 --- a/app/assets/alertify.js +++ /dev/null @@ -1 +0,0 @@ -!function(){"use strict";function t(){var t={parent:document.body,version:"1.0.11",defaultOkLabel:"Ok",okLabel:"Ok",defaultCancelLabel:"Cancel",cancelLabel:"Cancel",defaultMaxLogItems:2,maxLogItems:2,promptValue:"",promptPlaceholder:"",closeLogOnClick:!1,closeLogOnClickDefault:!1,delay:5e3,defaultDelay:5e3,logContainerClass:"alertify-logs",logContainerDefaultClass:"alertify-logs",dialogs:{buttons:{holder:"",ok:"",cancel:""},input:"",message:"

{{message}}

",log:"
{{message}}
"},defaultDialogs:{buttons:{holder:"",ok:"",cancel:""},input:"",message:"

{{message}}

",log:"
{{message}}
"},build:function(t){var e=this.dialogs.buttons.ok,o="
"+this.dialogs.message.replace("{{message}}",t.message);return"confirm"!==t.type&&"prompt"!==t.type||(e=this.dialogs.buttons.cancel+this.dialogs.buttons.ok),"prompt"===t.type&&(o+=this.dialogs.input),o=(o+this.dialogs.buttons.holder+"
").replace("{{buttons}}",e).replace("{{ok}}",this.okLabel).replace("{{cancel}}",this.cancelLabel)},setCloseLogOnClick:function(t){this.closeLogOnClick=!!t},close:function(t,e){this.closeLogOnClick&&t.addEventListener("click",function(){o(t)}),e=e&&!isNaN(+e)?+e:this.delay,0>e?o(t):e>0&&setTimeout(function(){o(t)},e)},dialog:function(t,e,o,n){return this.setup({type:e,message:t,onOkay:o,onCancel:n})},log:function(t,e,o){var n=document.querySelectorAll(".alertify-logs > div");if(n){var i=n.length-this.maxLogItems;if(i>=0)for(var a=0,l=i+1;l>a;a++)this.close(n[a],-1)}this.notify(t,e,o)},setLogPosition:function(t){this.logContainerClass="alertify-logs "+t},setupLogContainer:function(){var t=document.querySelector(".alertify-logs"),e=this.logContainerClass;return t||(t=document.createElement("div"),t.className=e,this.parent.appendChild(t)),t.className!==e&&(t.className=e),t},notify:function(e,o,n){var i=this.setupLogContainer(),a=document.createElement("div");a.className=o||"default",t.logTemplateMethod?a.innerHTML=t.logTemplateMethod(e):a.innerHTML=e,"function"==typeof n&&a.addEventListener("click",n),i.appendChild(a),setTimeout(function(){a.className+=" show"},10),this.close(a,this.delay)},setup:function(t){function e(e){"function"!=typeof e&&(e=function(){}),i&&i.addEventListener("click",function(i){t.onOkay&&"function"==typeof t.onOkay&&(l?t.onOkay(l.value,i):t.onOkay(i)),e(l?{buttonClicked:"ok",inputValue:l.value,event:i}:{buttonClicked:"ok",event:i}),o(n)}),a&&a.addEventListener("click",function(i){t.onCancel&&"function"==typeof t.onCancel&&t.onCancel(i),e({buttonClicked:"cancel",event:i}),o(n)}),l&&l.addEventListener("keyup",function(t){13===t.which&&i.click()})}var n=document.createElement("div");n.className="alertify hide",n.innerHTML=this.build(t);var i=n.querySelector(".ok"),a=n.querySelector(".cancel"),l=n.querySelector("input"),s=n.querySelector("label");l&&("string"==typeof this.promptPlaceholder&&(s?s.textContent=this.promptPlaceholder:l.placeholder=this.promptPlaceholder),"string"==typeof this.promptValue&&(l.value=this.promptValue));var r;return"function"==typeof Promise?r=new Promise(e):e(),this.parent.appendChild(n),setTimeout(function(){n.classList.remove("hide"),l&&t.type&&"prompt"===t.type?(l.select(),l.focus()):i&&i.focus()},100),r},okBtn:function(t){return this.okLabel=t,this},setDelay:function(t){return t=t||0,this.delay=isNaN(t)?this.defaultDelay:parseInt(t,10),this},cancelBtn:function(t){return this.cancelLabel=t,this},setMaxLogItems:function(t){this.maxLogItems=parseInt(t||this.defaultMaxLogItems)},theme:function(t){switch(t.toLowerCase()){case"bootstrap":this.dialogs.buttons.ok="",this.dialogs.buttons.cancel="",this.dialogs.input="";break;case"purecss":this.dialogs.buttons.ok="",this.dialogs.buttons.cancel="";break;case"mdl":case"material-design-light":this.dialogs.buttons.ok="",this.dialogs.buttons.cancel="",this.dialogs.input="
";break;case"angular-material":this.dialogs.buttons.ok="",this.dialogs.buttons.cancel="",this.dialogs.input="
";break;case"default":default:this.dialogs.buttons.ok=this.defaultDialogs.buttons.ok,this.dialogs.buttons.cancel=this.defaultDialogs.buttons.cancel,this.dialogs.input=this.defaultDialogs.input}},reset:function(){this.parent=document.body,this.theme("default"),this.okBtn(this.defaultOkLabel),this.cancelBtn(this.defaultCancelLabel),this.setMaxLogItems(),this.promptValue="",this.promptPlaceholder="",this.delay=this.defaultDelay,this.setCloseLogOnClick(this.closeLogOnClickDefault),this.setLogPosition("bottom left"),this.logTemplateMethod=null},injectCSS:function(){if(!document.querySelector("#alertifyCSS")){var t=document.getElementsByTagName("head")[0],e=document.createElement("style");e.type="text/css",e.id="alertifyCSS",e.innerHTML=".alertify-logs>*{padding:12px 24px;color:#fff;box-shadow:0 2px 5px 0 rgba(0,0,0,.2);border-radius:1px}.alertify-logs>*,.alertify-logs>.default{background:rgba(0,0,0,.8)}.alertify-logs>.error{background:rgba(244,67,54,.8)}.alertify-logs>.success{background:rgba(76,175,80,.9)}.alertify{position:fixed;background-color:rgba(0,0,0,.3);left:0;right:0;top:0;bottom:0;width:100%;height:100%;z-index:1}.alertify.hide{opacity:0;pointer-events:none}.alertify,.alertify.show{box-sizing:border-box;transition:all .33s cubic-bezier(.25,.8,.25,1)}.alertify,.alertify *{box-sizing:border-box}.alertify .dialog{padding:12px}.alertify .alert,.alertify .dialog{width:100%;margin:0 auto;position:relative;top:50%;transform:translateY(-50%)}.alertify .alert>*,.alertify .dialog>*{width:400px;max-width:95%;margin:0 auto;text-align:center;padding:12px;background:#fff;box-shadow:0 2px 4px -1px rgba(0,0,0,.14),0 4px 5px 0 rgba(0,0,0,.098),0 1px 10px 0 rgba(0,0,0,.084)}.alertify .alert .msg,.alertify .dialog .msg{padding:12px;margin-bottom:12px;margin:0;text-align:left}.alertify .alert input:not(.form-control),.alertify .dialog input:not(.form-control){margin-bottom:15px;width:100%;font-size:100%;padding:12px}.alertify .alert input:not(.form-control):focus,.alertify .dialog input:not(.form-control):focus{outline-offset:-2px}.alertify .alert nav,.alertify .dialog nav{text-align:right}.alertify .alert nav button:not(.btn):not(.pure-button):not(.md-button):not(.mdl-button),.alertify .dialog nav button:not(.btn):not(.pure-button):not(.md-button):not(.mdl-button){background:transparent;box-sizing:border-box;color:rgba(0,0,0,.87);position:relative;outline:0;border:0;display:inline-block;-ms-flex-align:center;-ms-grid-row-align:center;align-items:center;padding:0 6px;margin:6px 8px;line-height:36px;min-height:36px;white-space:nowrap;min-width:88px;text-align:center;text-transform:uppercase;font-size:14px;text-decoration:none;cursor:pointer;border:1px solid transparent;border-radius:2px}.alertify .alert nav button:not(.btn):not(.pure-button):not(.md-button):not(.mdl-button):active,.alertify .alert nav button:not(.btn):not(.pure-button):not(.md-button):not(.mdl-button):hover,.alertify .dialog nav button:not(.btn):not(.pure-button):not(.md-button):not(.mdl-button):active,.alertify .dialog nav button:not(.btn):not(.pure-button):not(.md-button):not(.mdl-button):hover{background-color:rgba(0,0,0,.05)}.alertify .alert nav button:not(.btn):not(.pure-button):not(.md-button):not(.mdl-button):focus,.alertify .dialog nav button:not(.btn):not(.pure-button):not(.md-button):not(.mdl-button):focus{border:1px solid rgba(0,0,0,.1)}.alertify .alert nav button.btn,.alertify .dialog nav button.btn{margin:6px 4px}.alertify-logs{position:fixed;z-index:1}.alertify-logs.bottom,.alertify-logs:not(.top){bottom:16px}.alertify-logs.left,.alertify-logs:not(.right){left:16px}.alertify-logs.left>*,.alertify-logs:not(.right)>*{float:left;transform:translateZ(0);height:auto}.alertify-logs.left>.show,.alertify-logs:not(.right)>.show{left:0}.alertify-logs.left>*,.alertify-logs.left>.hide,.alertify-logs:not(.right)>*,.alertify-logs:not(.right)>.hide{left:-110%}.alertify-logs.right{right:16px}.alertify-logs.right>*{float:right;transform:translateZ(0)}.alertify-logs.right>.show{right:0;opacity:1}.alertify-logs.right>*,.alertify-logs.right>.hide{right:-110%;opacity:0}.alertify-logs.top{top:0}.alertify-logs>*{box-sizing:border-box;transition:all .4s cubic-bezier(.25,.8,.25,1);position:relative;clear:both;backface-visibility:hidden;perspective:1000;max-height:0;margin:0;padding:0;overflow:hidden;opacity:0;pointer-events:none}.alertify-logs>.show{margin-top:12px;opacity:1;max-height:1000px;padding:12px;pointer-events:auto}",t.insertBefore(e,t.firstChild)}},removeCSS:function(){var t=document.querySelector("#alertifyCSS");t&&t.parentNode&&t.parentNode.removeChild(t)}};return t.injectCSS(),{_$$alertify:t,parent:function(e){t.parent=e},reset:function(){return t.reset(),this},alert:function(e,o,n){return t.dialog(e,"alert",o,n)||this},confirm:function(e,o,n){return t.dialog(e,"confirm",o,n)||this},prompt:function(e,o,n){return t.dialog(e,"prompt",o,n)||this},log:function(e,o){return t.log(e,"default",o),this},theme:function(e){return t.theme(e),this},success:function(e,o){return t.log(e,"success",o),this},error:function(e,o){return t.log(e,"error",o),this},cancelBtn:function(e){return t.cancelBtn(e),this},okBtn:function(e){return t.okBtn(e),this},delay:function(e){return t.setDelay(e),this},placeholder:function(e){return t.promptPlaceholder=e,this},defaultValue:function(e){return t.promptValue=e,this},maxLogItems:function(e){return t.setMaxLogItems(e),this},closeLogOnClick:function(e){return t.setCloseLogOnClick(!!e),this},logPosition:function(e){return t.setLogPosition(e||""),this},setLogTemplate:function(e){return t.logTemplateMethod=e,this},clearLogs:function(){return t.setupLogContainer().innerHTML="",this},version:t.version}}var e=500,o=function(t){if(t){var o=function(){t&&t.parentNode&&t.parentNode.removeChild(t)};t.classList.remove("show"),t.classList.add("hide"),t.addEventListener("transitionend",o),setTimeout(o,e)}};if("undefined"!=typeof module&&module&&module.exports){module.exports=function(){return new t};var n=new t;for(var i in n)module.exports[i]=n[i]}else"function"==typeof define&&define.amd?define(function(){return new t}):window.alertify=new t}(); \ No newline at end of file diff --git a/app/assets/bootstrap.css b/app/assets/bootstrap.css deleted file mode 100644 index f5c0e02..0000000 --- a/app/assets/bootstrap.css +++ /dev/null @@ -1,7523 +0,0 @@ -@import url("https://fonts.googleapis.com/css?family=Roboto:300,400,500,700"); -/*! - * bootswatch v3.3.6 - * Homepage: http://bootswatch.com - * Copyright 2012-2016 Thomas Park - * Licensed under MIT - * Based on Bootstrap -*/ -/*! - * Bootstrap v3.3.6 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ -/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ -html { - font-family: sans-serif; - -ms-text-size-adjust: 100%; - -webkit-text-size-adjust: 100%; -} -body { - margin: 0; -} -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -main, -menu, -nav, -section, -summary { - display: block; -} -audio, -canvas, -progress, -video { - display: inline-block; - vertical-align: baseline; -} -audio:not([controls]) { - display: none; - height: 0; -} -[hidden], -template { - display: none; -} -a { - background-color: transparent; -} -a:active, -a:hover { - outline: 0; -} -abbr[title] { - border-bottom: 1px dotted; -} -b, -strong { - font-weight: bold; -} -dfn { - font-style: italic; -} -h1 { - font-size: 2em; - margin: 0.67em 0; -} -mark { - background: #ff0; - color: #000; -} -small { - font-size: 80%; -} -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} -sup { - top: -0.5em; -} -sub { - bottom: -0.25em; -} -img { - border: 0; -} -svg:not(:root) { - overflow: hidden; -} -figure { - margin: 1em 40px; -} -hr { - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; - height: 0; -} -pre { - overflow: auto; -} -code, -kbd, -pre, -samp { - font-family: monospace, monospace; - font-size: 1em; -} -button, -input, -optgroup, -select, -textarea { - color: inherit; - font: inherit; - margin: 0; -} -button { - overflow: visible; -} -button, -select { - text-transform: none; -} -button, -html input[type="button"], -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; - cursor: pointer; -} -button[disabled], -html input[disabled] { - cursor: default; -} -button::-moz-focus-inner, -input::-moz-focus-inner { - border: 0; - padding: 0; -} -input { - line-height: normal; -} -input[type="checkbox"], -input[type="radio"] { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - padding: 0; -} -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - height: auto; -} -input[type="search"] { - -webkit-appearance: textfield; - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; -} -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} -legend { - border: 0; - padding: 0; -} -textarea { - overflow: auto; -} -optgroup { - font-weight: bold; -} -table { - border-collapse: collapse; - border-spacing: 0; -} -td, -th { - padding: 0; -} -/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ -@media print { - *, - *:before, - *:after { - background: transparent !important; - color: #000 !important; - -webkit-box-shadow: none !important; - box-shadow: none !important; - text-shadow: none !important; - } - a, - a:visited { - text-decoration: underline; - } - a[href]:after { - content: " (" attr(href) ")"; - } - abbr[title]:after { - content: " (" attr(title) ")"; - } - a[href^="#"]:after, - a[href^="javascript:"]:after { - content: ""; - } - pre, - blockquote { - border: 1px solid #999; - page-break-inside: avoid; - } - thead { - display: table-header-group; - } - tr, - img { - page-break-inside: avoid; - } - img { - max-width: 100% !important; - } - p, - h2, - h3 { - orphans: 3; - widows: 3; - } - h2, - h3 { - page-break-after: avoid; - } - .navbar { - display: none; - } - .btn > .caret, - .dropup > .btn > .caret { - border-top-color: #000 !important; - } - .label { - border: 1px solid #000; - } - .table { - border-collapse: collapse !important; - } - .table td, - .table th { - background-color: #fff !important; - } - .table-bordered th, - .table-bordered td { - border: 1px solid #ddd !important; - } -} -@font-face { - font-family: 'Glyphicons Halflings'; - src: url('../fonts/glyphicons-halflings-regular.eot'); - src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); -} -.glyphicon { - position: relative; - top: 1px; - display: inline-block; - font-family: 'Glyphicons Halflings'; - font-style: normal; - font-weight: normal; - line-height: 1; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -.glyphicon-asterisk:before { - content: "\002a"; -} -.glyphicon-plus:before { - content: "\002b"; -} -.glyphicon-euro:before, -.glyphicon-eur:before { - content: "\20ac"; -} -.glyphicon-minus:before { - content: "\2212"; -} -.glyphicon-cloud:before { - content: "\2601"; -} -.glyphicon-envelope:before { - content: "\2709"; -} -.glyphicon-pencil:before { - content: "\270f"; -} -.glyphicon-glass:before { - content: "\e001"; -} -.glyphicon-music:before { - content: "\e002"; -} -.glyphicon-search:before { - content: "\e003"; -} -.glyphicon-heart:before { - content: "\e005"; -} -.glyphicon-star:before { - content: "\e006"; -} -.glyphicon-star-empty:before { - content: "\e007"; -} -.glyphicon-user:before { - content: "\e008"; -} -.glyphicon-film:before { - content: "\e009"; -} -.glyphicon-th-large:before { - content: "\e010"; -} -.glyphicon-th:before { - content: "\e011"; -} -.glyphicon-th-list:before { - content: "\e012"; -} -.glyphicon-ok:before { - content: "\e013"; -} -.glyphicon-remove:before { - content: "\e014"; -} -.glyphicon-zoom-in:before { - content: "\e015"; -} -.glyphicon-zoom-out:before { - content: "\e016"; -} -.glyphicon-off:before { - content: "\e017"; -} -.glyphicon-signal:before { - content: "\e018"; -} -.glyphicon-cog:before { - content: "\e019"; -} -.glyphicon-trash:before { - content: "\e020"; -} -.glyphicon-home:before { - content: "\e021"; -} -.glyphicon-file:before { - content: "\e022"; -} -.glyphicon-time:before { - content: "\e023"; -} -.glyphicon-road:before { - content: "\e024"; -} -.glyphicon-download-alt:before { - content: "\e025"; -} -.glyphicon-download:before { - content: "\e026"; -} -.glyphicon-upload:before { - content: "\e027"; -} -.glyphicon-inbox:before { - content: "\e028"; -} -.glyphicon-play-circle:before { - content: "\e029"; -} -.glyphicon-repeat:before { - content: "\e030"; -} -.glyphicon-refresh:before { - content: "\e031"; -} -.glyphicon-list-alt:before { - content: "\e032"; -} -.glyphicon-lock:before { - content: "\e033"; -} -.glyphicon-flag:before { - content: "\e034"; -} -.glyphicon-headphones:before { - content: "\e035"; -} -.glyphicon-volume-off:before { - content: "\e036"; -} -.glyphicon-volume-down:before { - content: "\e037"; -} -.glyphicon-volume-up:before { - content: "\e038"; -} -.glyphicon-qrcode:before { - content: "\e039"; -} -.glyphicon-barcode:before { - content: "\e040"; -} -.glyphicon-tag:before { - content: "\e041"; -} -.glyphicon-tags:before { - content: "\e042"; -} -.glyphicon-book:before { - content: "\e043"; -} -.glyphicon-bookmark:before { - content: "\e044"; -} -.glyphicon-print:before { - content: "\e045"; -} -.glyphicon-camera:before { - content: "\e046"; -} -.glyphicon-font:before { - content: "\e047"; -} -.glyphicon-bold:before { - content: "\e048"; -} -.glyphicon-italic:before { - content: "\e049"; -} -.glyphicon-text-height:before { - content: "\e050"; -} -.glyphicon-text-width:before { - content: "\e051"; -} -.glyphicon-align-left:before { - content: "\e052"; -} -.glyphicon-align-center:before { - content: "\e053"; -} -.glyphicon-align-right:before { - content: "\e054"; -} -.glyphicon-align-justify:before { - content: "\e055"; -} -.glyphicon-list:before { - content: "\e056"; -} -.glyphicon-indent-left:before { - content: "\e057"; -} -.glyphicon-indent-right:before { - content: "\e058"; -} -.glyphicon-facetime-video:before { - content: "\e059"; -} -.glyphicon-picture:before { - content: "\e060"; -} -.glyphicon-map-marker:before { - content: "\e062"; -} -.glyphicon-adjust:before { - content: "\e063"; -} -.glyphicon-tint:before { - content: "\e064"; -} -.glyphicon-edit:before { - content: "\e065"; -} -.glyphicon-share:before { - content: "\e066"; -} -.glyphicon-check:before { - content: "\e067"; -} -.glyphicon-move:before { - content: "\e068"; -} -.glyphicon-step-backward:before { - content: "\e069"; -} -.glyphicon-fast-backward:before { - content: "\e070"; -} -.glyphicon-backward:before { - content: "\e071"; -} -.glyphicon-play:before { - content: "\e072"; -} -.glyphicon-pause:before { - content: "\e073"; -} -.glyphicon-stop:before { - content: "\e074"; -} -.glyphicon-forward:before { - content: "\e075"; -} -.glyphicon-fast-forward:before { - content: "\e076"; -} -.glyphicon-step-forward:before { - content: "\e077"; -} -.glyphicon-eject:before { - content: "\e078"; -} -.glyphicon-chevron-left:before { - content: "\e079"; -} -.glyphicon-chevron-right:before { - content: "\e080"; -} -.glyphicon-plus-sign:before { - content: "\e081"; -} -.glyphicon-minus-sign:before { - content: "\e082"; -} -.glyphicon-remove-sign:before { - content: "\e083"; -} -.glyphicon-ok-sign:before { - content: "\e084"; -} -.glyphicon-question-sign:before { - content: "\e085"; -} -.glyphicon-info-sign:before { - content: "\e086"; -} -.glyphicon-screenshot:before { - content: "\e087"; -} -.glyphicon-remove-circle:before { - content: "\e088"; -} -.glyphicon-ok-circle:before { - content: "\e089"; -} -.glyphicon-ban-circle:before { - content: "\e090"; -} -.glyphicon-arrow-left:before { - content: "\e091"; -} -.glyphicon-arrow-right:before { - content: "\e092"; -} -.glyphicon-arrow-up:before { - content: "\e093"; -} -.glyphicon-arrow-down:before { - content: "\e094"; -} -.glyphicon-share-alt:before { - content: "\e095"; -} -.glyphicon-resize-full:before { - content: "\e096"; -} -.glyphicon-resize-small:before { - content: "\e097"; -} -.glyphicon-exclamation-sign:before { - content: "\e101"; -} -.glyphicon-gift:before { - content: "\e102"; -} -.glyphicon-leaf:before { - content: "\e103"; -} -.glyphicon-fire:before { - content: "\e104"; -} -.glyphicon-eye-open:before { - content: "\e105"; -} -.glyphicon-eye-close:before { - content: "\e106"; -} -.glyphicon-warning-sign:before { - content: "\e107"; -} -.glyphicon-plane:before { - content: "\e108"; -} -.glyphicon-calendar:before { - content: "\e109"; -} -.glyphicon-random:before { - content: "\e110"; -} -.glyphicon-comment:before { - content: "\e111"; -} -.glyphicon-magnet:before { - content: "\e112"; -} -.glyphicon-chevron-up:before { - content: "\e113"; -} -.glyphicon-chevron-down:before { - content: "\e114"; -} -.glyphicon-retweet:before { - content: "\e115"; -} -.glyphicon-shopping-cart:before { - content: "\e116"; -} -.glyphicon-folder-close:before { - content: "\e117"; -} -.glyphicon-folder-open:before { - content: "\e118"; -} -.glyphicon-resize-vertical:before { - content: "\e119"; -} -.glyphicon-resize-horizontal:before { - content: "\e120"; -} -.glyphicon-hdd:before { - content: "\e121"; -} -.glyphicon-bullhorn:before { - content: "\e122"; -} -.glyphicon-bell:before { - content: "\e123"; -} -.glyphicon-certificate:before { - content: "\e124"; -} -.glyphicon-thumbs-up:before { - content: "\e125"; -} -.glyphicon-thumbs-down:before { - content: "\e126"; -} -.glyphicon-hand-right:before { - content: "\e127"; -} -.glyphicon-hand-left:before { - content: "\e128"; -} -.glyphicon-hand-up:before { - content: "\e129"; -} -.glyphicon-hand-down:before { - content: "\e130"; -} -.glyphicon-circle-arrow-right:before { - content: "\e131"; -} -.glyphicon-circle-arrow-left:before { - content: "\e132"; -} -.glyphicon-circle-arrow-up:before { - content: "\e133"; -} -.glyphicon-circle-arrow-down:before { - content: "\e134"; -} -.glyphicon-globe:before { - content: "\e135"; -} -.glyphicon-wrench:before { - content: "\e136"; -} -.glyphicon-tasks:before { - content: "\e137"; -} -.glyphicon-filter:before { - content: "\e138"; -} -.glyphicon-briefcase:before { - content: "\e139"; -} -.glyphicon-fullscreen:before { - content: "\e140"; -} -.glyphicon-dashboard:before { - content: "\e141"; -} -.glyphicon-paperclip:before { - content: "\e142"; -} -.glyphicon-heart-empty:before { - content: "\e143"; -} -.glyphicon-link:before { - content: "\e144"; -} -.glyphicon-phone:before { - content: "\e145"; -} -.glyphicon-pushpin:before { - content: "\e146"; -} -.glyphicon-usd:before { - content: "\e148"; -} -.glyphicon-gbp:before { - content: "\e149"; -} -.glyphicon-sort:before { - content: "\e150"; -} -.glyphicon-sort-by-alphabet:before { - content: "\e151"; -} -.glyphicon-sort-by-alphabet-alt:before { - content: "\e152"; -} -.glyphicon-sort-by-order:before { - content: "\e153"; -} -.glyphicon-sort-by-order-alt:before { - content: "\e154"; -} -.glyphicon-sort-by-attributes:before { - content: "\e155"; -} -.glyphicon-sort-by-attributes-alt:before { - content: "\e156"; -} -.glyphicon-unchecked:before { - content: "\e157"; -} -.glyphicon-expand:before { - content: "\e158"; -} -.glyphicon-collapse-down:before { - content: "\e159"; -} -.glyphicon-collapse-up:before { - content: "\e160"; -} -.glyphicon-log-in:before { - content: "\e161"; -} -.glyphicon-flash:before { - content: "\e162"; -} -.glyphicon-log-out:before { - content: "\e163"; -} -.glyphicon-new-window:before { - content: "\e164"; -} -.glyphicon-record:before { - content: "\e165"; -} -.glyphicon-save:before { - content: "\e166"; -} -.glyphicon-open:before { - content: "\e167"; -} -.glyphicon-saved:before { - content: "\e168"; -} -.glyphicon-import:before { - content: "\e169"; -} -.glyphicon-export:before { - content: "\e170"; -} -.glyphicon-send:before { - content: "\e171"; -} -.glyphicon-floppy-disk:before { - content: "\e172"; -} -.glyphicon-floppy-saved:before { - content: "\e173"; -} -.glyphicon-floppy-remove:before { - content: "\e174"; -} -.glyphicon-floppy-save:before { - content: "\e175"; -} -.glyphicon-floppy-open:before { - content: "\e176"; -} -.glyphicon-credit-card:before { - content: "\e177"; -} -.glyphicon-transfer:before { - content: "\e178"; -} -.glyphicon-cutlery:before { - content: "\e179"; -} -.glyphicon-header:before { - content: "\e180"; -} -.glyphicon-compressed:before { - content: "\e181"; -} -.glyphicon-earphone:before { - content: "\e182"; -} -.glyphicon-phone-alt:before { - content: "\e183"; -} -.glyphicon-tower:before { - content: "\e184"; -} -.glyphicon-stats:before { - content: "\e185"; -} -.glyphicon-sd-video:before { - content: "\e186"; -} -.glyphicon-hd-video:before { - content: "\e187"; -} -.glyphicon-subtitles:before { - content: "\e188"; -} -.glyphicon-sound-stereo:before { - content: "\e189"; -} -.glyphicon-sound-dolby:before { - content: "\e190"; -} -.glyphicon-sound-5-1:before { - content: "\e191"; -} -.glyphicon-sound-6-1:before { - content: "\e192"; -} -.glyphicon-sound-7-1:before { - content: "\e193"; -} -.glyphicon-copyright-mark:before { - content: "\e194"; -} -.glyphicon-registration-mark:before { - content: "\e195"; -} -.glyphicon-cloud-download:before { - content: "\e197"; -} -.glyphicon-cloud-upload:before { - content: "\e198"; -} -.glyphicon-tree-conifer:before { - content: "\e199"; -} -.glyphicon-tree-deciduous:before { - content: "\e200"; -} -.glyphicon-cd:before { - content: "\e201"; -} -.glyphicon-save-file:before { - content: "\e202"; -} -.glyphicon-open-file:before { - content: "\e203"; -} -.glyphicon-level-up:before { - content: "\e204"; -} -.glyphicon-copy:before { - content: "\e205"; -} -.glyphicon-paste:before { - content: "\e206"; -} -.glyphicon-alert:before { - content: "\e209"; -} -.glyphicon-equalizer:before { - content: "\e210"; -} -.glyphicon-king:before { - content: "\e211"; -} -.glyphicon-queen:before { - content: "\e212"; -} -.glyphicon-pawn:before { - content: "\e213"; -} -.glyphicon-bishop:before { - content: "\e214"; -} -.glyphicon-knight:before { - content: "\e215"; -} -.glyphicon-baby-formula:before { - content: "\e216"; -} -.glyphicon-tent:before { - content: "\26fa"; -} -.glyphicon-blackboard:before { - content: "\e218"; -} -.glyphicon-bed:before { - content: "\e219"; -} -.glyphicon-apple:before { - content: "\f8ff"; -} -.glyphicon-erase:before { - content: "\e221"; -} -.glyphicon-hourglass:before { - content: "\231b"; -} -.glyphicon-lamp:before { - content: "\e223"; -} -.glyphicon-duplicate:before { - content: "\e224"; -} -.glyphicon-piggy-bank:before { - content: "\e225"; -} -.glyphicon-scissors:before { - content: "\e226"; -} -.glyphicon-bitcoin:before { - content: "\e227"; -} -.glyphicon-btc:before { - content: "\e227"; -} -.glyphicon-xbt:before { - content: "\e227"; -} -.glyphicon-yen:before { - content: "\00a5"; -} -.glyphicon-jpy:before { - content: "\00a5"; -} -.glyphicon-ruble:before { - content: "\20bd"; -} -.glyphicon-rub:before { - content: "\20bd"; -} -.glyphicon-scale:before { - content: "\e230"; -} -.glyphicon-ice-lolly:before { - content: "\e231"; -} -.glyphicon-ice-lolly-tasted:before { - content: "\e232"; -} -.glyphicon-education:before { - content: "\e233"; -} -.glyphicon-option-horizontal:before { - content: "\e234"; -} -.glyphicon-option-vertical:before { - content: "\e235"; -} -.glyphicon-menu-hamburger:before { - content: "\e236"; -} -.glyphicon-modal-window:before { - content: "\e237"; -} -.glyphicon-oil:before { - content: "\e238"; -} -.glyphicon-grain:before { - content: "\e239"; -} -.glyphicon-sunglasses:before { - content: "\e240"; -} -.glyphicon-text-size:before { - content: "\e241"; -} -.glyphicon-text-color:before { - content: "\e242"; -} -.glyphicon-text-background:before { - content: "\e243"; -} -.glyphicon-object-align-top:before { - content: "\e244"; -} -.glyphicon-object-align-bottom:before { - content: "\e245"; -} -.glyphicon-object-align-horizontal:before { - content: "\e246"; -} -.glyphicon-object-align-left:before { - content: "\e247"; -} -.glyphicon-object-align-vertical:before { - content: "\e248"; -} -.glyphicon-object-align-right:before { - content: "\e249"; -} -.glyphicon-triangle-right:before { - content: "\e250"; -} -.glyphicon-triangle-left:before { - content: "\e251"; -} -.glyphicon-triangle-bottom:before { - content: "\e252"; -} -.glyphicon-triangle-top:before { - content: "\e253"; -} -.glyphicon-console:before { - content: "\e254"; -} -.glyphicon-superscript:before { - content: "\e255"; -} -.glyphicon-subscript:before { - content: "\e256"; -} -.glyphicon-menu-left:before { - content: "\e257"; -} -.glyphicon-menu-right:before { - content: "\e258"; -} -.glyphicon-menu-down:before { - content: "\e259"; -} -.glyphicon-menu-up:before { - content: "\e260"; -} -* { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -*:before, -*:after { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -html { - font-size: 10px; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); -} -body { - font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 13px; - line-height: 1.846; - color: #666666; - background-color: #ffffff; -} -input, -button, -select, -textarea { - font-family: inherit; - font-size: inherit; - line-height: inherit; -} -a { - color: #2196f3; - text-decoration: none; -} -a:hover, -a:focus { - color: #0a6ebd; - text-decoration: underline; -} -a:focus { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} -figure { - margin: 0; -} -img { - vertical-align: middle; -} -.img-responsive, -.thumbnail > img, -.thumbnail a > img, -.carousel-inner > .item > img, -.carousel-inner > .item > a > img { - display: block; - max-width: 100%; - height: auto; -} -.img-rounded { - border-radius: 3px; -} -.img-thumbnail { - padding: 4px; - line-height: 1.846; - background-color: #ffffff; - border: 1px solid #dddddd; - border-radius: 3px; - -webkit-transition: all 0.2s ease-in-out; - -o-transition: all 0.2s ease-in-out; - transition: all 0.2s ease-in-out; - display: inline-block; - max-width: 100%; - height: auto; -} -.img-circle { - border-radius: 50%; -} -hr { - margin-top: 23px; - margin-bottom: 23px; - border: 0; - border-top: 1px solid #eeeeee; -} -.sr-only { - position: absolute; - width: 1px; - height: 1px; - margin: -1px; - padding: 0; - overflow: hidden; - clip: rect(0, 0, 0, 0); - border: 0; -} -.sr-only-focusable:active, -.sr-only-focusable:focus { - position: static; - width: auto; - height: auto; - margin: 0; - overflow: visible; - clip: auto; -} -[role="button"] { - cursor: pointer; -} -h1, -h2, -h3, -h4, -h5, -h6, -.h1, -.h2, -.h3, -.h4, -.h5, -.h6 { - font-family: inherit; - font-weight: 400; - line-height: 1.1; - color: #444444; -} -h1 small, -h2 small, -h3 small, -h4 small, -h5 small, -h6 small, -.h1 small, -.h2 small, -.h3 small, -.h4 small, -.h5 small, -.h6 small, -h1 .small, -h2 .small, -h3 .small, -h4 .small, -h5 .small, -h6 .small, -.h1 .small, -.h2 .small, -.h3 .small, -.h4 .small, -.h5 .small, -.h6 .small { - font-weight: normal; - line-height: 1; - color: #bbbbbb; -} -h1, -.h1, -h2, -.h2, -h3, -.h3 { - margin-top: 23px; - margin-bottom: 11.5px; -} -h1 small, -.h1 small, -h2 small, -.h2 small, -h3 small, -.h3 small, -h1 .small, -.h1 .small, -h2 .small, -.h2 .small, -h3 .small, -.h3 .small { - font-size: 65%; -} -h4, -.h4, -h5, -.h5, -h6, -.h6 { - margin-top: 11.5px; - margin-bottom: 11.5px; -} -h4 small, -.h4 small, -h5 small, -.h5 small, -h6 small, -.h6 small, -h4 .small, -.h4 .small, -h5 .small, -.h5 .small, -h6 .small, -.h6 .small { - font-size: 75%; -} -h1, -.h1 { - font-size: 56px; -} -h2, -.h2 { - font-size: 45px; -} -h3, -.h3 { - font-size: 34px; -} -h4, -.h4 { - font-size: 24px; -} -h5, -.h5 { - font-size: 20px; -} -h6, -.h6 { - font-size: 14px; -} -p { - margin: 0 0 11.5px; -} -.lead { - margin-bottom: 23px; - font-size: 14px; - font-weight: 300; - line-height: 1.4; -} -@media (min-width: 768px) { - .lead { - font-size: 19.5px; - } -} -small, -.small { - font-size: 92%; -} -mark, -.mark { - background-color: #ffe0b2; - padding: .2em; -} -.text-left { - text-align: left; -} -.text-right { - text-align: right; -} -.text-center { - text-align: center; -} -.text-justify { - text-align: justify; -} -.text-nowrap { - white-space: nowrap; -} -.text-lowercase { - text-transform: lowercase; -} -.text-uppercase { - text-transform: uppercase; -} -.text-capitalize { - text-transform: capitalize; -} -.text-muted { - color: #bbbbbb; -} -.text-primary { - color: #2196f3; -} -a.text-primary:hover, -a.text-primary:focus { - color: #0c7cd5; -} -.text-success { - color: #4caf50; -} -a.text-success:hover, -a.text-success:focus { - color: #3d8b40; -} -.text-info { - color: #9c27b0; -} -a.text-info:hover, -a.text-info:focus { - color: #771e86; -} -.text-warning { - color: #ff9800; -} -a.text-warning:hover, -a.text-warning:focus { - color: #cc7a00; -} -.text-danger { - color: #e51c23; -} -a.text-danger:hover, -a.text-danger:focus { - color: #b9151b; -} -.bg-primary { - color: #fff; - background-color: #2196f3; -} -a.bg-primary:hover, -a.bg-primary:focus { - background-color: #0c7cd5; -} -.bg-success { - background-color: #dff0d8; -} -a.bg-success:hover, -a.bg-success:focus { - background-color: #c1e2b3; -} -.bg-info { - background-color: #e1bee7; -} -a.bg-info:hover, -a.bg-info:focus { - background-color: #d099d9; -} -.bg-warning { - background-color: #ffe0b2; -} -a.bg-warning:hover, -a.bg-warning:focus { - background-color: #ffcb7f; -} -.bg-danger { - background-color: #f9bdbb; -} -a.bg-danger:hover, -a.bg-danger:focus { - background-color: #f5908c; -} -.page-header { - padding-bottom: 10.5px; - margin: 46px 0 23px; - border-bottom: 1px solid #eeeeee; -} -ul, -ol { - margin-top: 0; - margin-bottom: 11.5px; -} -ul ul, -ol ul, -ul ol, -ol ol { - margin-bottom: 0; -} -.list-unstyled { - padding-left: 0; - list-style: none; -} -.list-inline { - padding-left: 0; - list-style: none; - margin-left: -5px; -} -.list-inline > li { - display: inline-block; - padding-left: 5px; - padding-right: 5px; -} -dl { - margin-top: 0; - margin-bottom: 23px; -} -dt, -dd { - line-height: 1.846; -} -dt { - font-weight: bold; -} -dd { - margin-left: 0; -} -@media (min-width: 768px) { - .dl-horizontal dt { - float: left; - width: 160px; - clear: left; - text-align: right; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .dl-horizontal dd { - margin-left: 180px; - } -} -abbr[title], -abbr[data-original-title] { - cursor: help; - border-bottom: 1px dotted #bbbbbb; -} -.initialism { - font-size: 90%; - text-transform: uppercase; -} -blockquote { - padding: 11.5px 23px; - margin: 0 0 23px; - font-size: 16.25px; - border-left: 5px solid #eeeeee; -} -blockquote p:last-child, -blockquote ul:last-child, -blockquote ol:last-child { - margin-bottom: 0; -} -blockquote footer, -blockquote small, -blockquote .small { - display: block; - font-size: 80%; - line-height: 1.846; - color: #bbbbbb; -} -blockquote footer:before, -blockquote small:before, -blockquote .small:before { - content: '\2014 \00A0'; -} -.blockquote-reverse, -blockquote.pull-right { - padding-right: 15px; - padding-left: 0; - border-right: 5px solid #eeeeee; - border-left: 0; - text-align: right; -} -.blockquote-reverse footer:before, -blockquote.pull-right footer:before, -.blockquote-reverse small:before, -blockquote.pull-right small:before, -.blockquote-reverse .small:before, -blockquote.pull-right .small:before { - content: ''; -} -.blockquote-reverse footer:after, -blockquote.pull-right footer:after, -.blockquote-reverse small:after, -blockquote.pull-right small:after, -.blockquote-reverse .small:after, -blockquote.pull-right .small:after { - content: '\00A0 \2014'; -} -address { - margin-bottom: 23px; - font-style: normal; - line-height: 1.846; -} -code, -kbd, -pre, -samp { - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; -} -code { - padding: 2px 4px; - font-size: 90%; - color: #c7254e; - background-color: #f9f2f4; - border-radius: 3px; -} -kbd { - padding: 2px 4px; - font-size: 90%; - color: #ffffff; - background-color: #333333; - border-radius: 3px; - -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); -} -kbd kbd { - padding: 0; - font-size: 100%; - font-weight: bold; - -webkit-box-shadow: none; - box-shadow: none; -} -pre { - display: block; - padding: 11px; - margin: 0 0 11.5px; - font-size: 12px; - line-height: 1.846; - word-break: break-all; - word-wrap: break-word; - color: #212121; - background-color: #f5f5f5; - border: 1px solid #cccccc; - border-radius: 3px; -} -pre code { - padding: 0; - font-size: inherit; - color: inherit; - white-space: pre-wrap; - background-color: transparent; - border-radius: 0; -} -.pre-scrollable { - max-height: 340px; - overflow-y: scroll; -} -.container { - margin-right: auto; - margin-left: auto; - padding-left: 15px; - padding-right: 15px; -} -@media (min-width: 768px) { - .container { - width: 750px; - } -} -@media (min-width: 992px) { - .container { - width: 970px; - } -} -@media (min-width: 1200px) { - .container { - width: 1170px; - } -} -.container-fluid { - margin-right: auto; - margin-left: auto; - padding-left: 15px; - padding-right: 15px; -} -.row { - margin-left: -15px; - margin-right: -15px; -} -.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { - position: relative; - min-height: 1px; - padding-left: 15px; - padding-right: 15px; -} -.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { - float: left; -} -.col-xs-12 { - width: 100%; -} -.col-xs-11 { - width: 91.66666667%; -} -.col-xs-10 { - width: 83.33333333%; -} -.col-xs-9 { - width: 75%; -} -.col-xs-8 { - width: 66.66666667%; -} -.col-xs-7 { - width: 58.33333333%; -} -.col-xs-6 { - width: 50%; -} -.col-xs-5 { - width: 41.66666667%; -} -.col-xs-4 { - width: 33.33333333%; -} -.col-xs-3 { - width: 25%; -} -.col-xs-2 { - width: 16.66666667%; -} -.col-xs-1 { - width: 8.33333333%; -} -.col-xs-pull-12 { - right: 100%; -} -.col-xs-pull-11 { - right: 91.66666667%; -} -.col-xs-pull-10 { - right: 83.33333333%; -} -.col-xs-pull-9 { - right: 75%; -} -.col-xs-pull-8 { - right: 66.66666667%; -} -.col-xs-pull-7 { - right: 58.33333333%; -} -.col-xs-pull-6 { - right: 50%; -} -.col-xs-pull-5 { - right: 41.66666667%; -} -.col-xs-pull-4 { - right: 33.33333333%; -} -.col-xs-pull-3 { - right: 25%; -} -.col-xs-pull-2 { - right: 16.66666667%; -} -.col-xs-pull-1 { - right: 8.33333333%; -} -.col-xs-pull-0 { - right: auto; -} -.col-xs-push-12 { - left: 100%; -} -.col-xs-push-11 { - left: 91.66666667%; -} -.col-xs-push-10 { - left: 83.33333333%; -} -.col-xs-push-9 { - left: 75%; -} -.col-xs-push-8 { - left: 66.66666667%; -} -.col-xs-push-7 { - left: 58.33333333%; -} -.col-xs-push-6 { - left: 50%; -} -.col-xs-push-5 { - left: 41.66666667%; -} -.col-xs-push-4 { - left: 33.33333333%; -} -.col-xs-push-3 { - left: 25%; -} -.col-xs-push-2 { - left: 16.66666667%; -} -.col-xs-push-1 { - left: 8.33333333%; -} -.col-xs-push-0 { - left: auto; -} -.col-xs-offset-12 { - margin-left: 100%; -} -.col-xs-offset-11 { - margin-left: 91.66666667%; -} -.col-xs-offset-10 { - margin-left: 83.33333333%; -} -.col-xs-offset-9 { - margin-left: 75%; -} -.col-xs-offset-8 { - margin-left: 66.66666667%; -} -.col-xs-offset-7 { - margin-left: 58.33333333%; -} -.col-xs-offset-6 { - margin-left: 50%; -} -.col-xs-offset-5 { - margin-left: 41.66666667%; -} -.col-xs-offset-4 { - margin-left: 33.33333333%; -} -.col-xs-offset-3 { - margin-left: 25%; -} -.col-xs-offset-2 { - margin-left: 16.66666667%; -} -.col-xs-offset-1 { - margin-left: 8.33333333%; -} -.col-xs-offset-0 { - margin-left: 0%; -} -@media (min-width: 768px) { - .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { - float: left; - } - .col-sm-12 { - width: 100%; - } - .col-sm-11 { - width: 91.66666667%; - } - .col-sm-10 { - width: 83.33333333%; - } - .col-sm-9 { - width: 75%; - } - .col-sm-8 { - width: 66.66666667%; - } - .col-sm-7 { - width: 58.33333333%; - } - .col-sm-6 { - width: 50%; - } - .col-sm-5 { - width: 41.66666667%; - } - .col-sm-4 { - width: 33.33333333%; - } - .col-sm-3 { - width: 25%; - } - .col-sm-2 { - width: 16.66666667%; - } - .col-sm-1 { - width: 8.33333333%; - } - .col-sm-pull-12 { - right: 100%; - } - .col-sm-pull-11 { - right: 91.66666667%; - } - .col-sm-pull-10 { - right: 83.33333333%; - } - .col-sm-pull-9 { - right: 75%; - } - .col-sm-pull-8 { - right: 66.66666667%; - } - .col-sm-pull-7 { - right: 58.33333333%; - } - .col-sm-pull-6 { - right: 50%; - } - .col-sm-pull-5 { - right: 41.66666667%; - } - .col-sm-pull-4 { - right: 33.33333333%; - } - .col-sm-pull-3 { - right: 25%; - } - .col-sm-pull-2 { - right: 16.66666667%; - } - .col-sm-pull-1 { - right: 8.33333333%; - } - .col-sm-pull-0 { - right: auto; - } - .col-sm-push-12 { - left: 100%; - } - .col-sm-push-11 { - left: 91.66666667%; - } - .col-sm-push-10 { - left: 83.33333333%; - } - .col-sm-push-9 { - left: 75%; - } - .col-sm-push-8 { - left: 66.66666667%; - } - .col-sm-push-7 { - left: 58.33333333%; - } - .col-sm-push-6 { - left: 50%; - } - .col-sm-push-5 { - left: 41.66666667%; - } - .col-sm-push-4 { - left: 33.33333333%; - } - .col-sm-push-3 { - left: 25%; - } - .col-sm-push-2 { - left: 16.66666667%; - } - .col-sm-push-1 { - left: 8.33333333%; - } - .col-sm-push-0 { - left: auto; - } - .col-sm-offset-12 { - margin-left: 100%; - } - .col-sm-offset-11 { - margin-left: 91.66666667%; - } - .col-sm-offset-10 { - margin-left: 83.33333333%; - } - .col-sm-offset-9 { - margin-left: 75%; - } - .col-sm-offset-8 { - margin-left: 66.66666667%; - } - .col-sm-offset-7 { - margin-left: 58.33333333%; - } - .col-sm-offset-6 { - margin-left: 50%; - } - .col-sm-offset-5 { - margin-left: 41.66666667%; - } - .col-sm-offset-4 { - margin-left: 33.33333333%; - } - .col-sm-offset-3 { - margin-left: 25%; - } - .col-sm-offset-2 { - margin-left: 16.66666667%; - } - .col-sm-offset-1 { - margin-left: 8.33333333%; - } - .col-sm-offset-0 { - margin-left: 0%; - } -} -@media (min-width: 992px) { - .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { - float: left; - } - .col-md-12 { - width: 100%; - } - .col-md-11 { - width: 91.66666667%; - } - .col-md-10 { - width: 83.33333333%; - } - .col-md-9 { - width: 75%; - } - .col-md-8 { - width: 66.66666667%; - } - .col-md-7 { - width: 58.33333333%; - } - .col-md-6 { - width: 50%; - } - .col-md-5 { - width: 41.66666667%; - } - .col-md-4 { - width: 33.33333333%; - } - .col-md-3 { - width: 25%; - } - .col-md-2 { - width: 16.66666667%; - } - .col-md-1 { - width: 8.33333333%; - } - .col-md-pull-12 { - right: 100%; - } - .col-md-pull-11 { - right: 91.66666667%; - } - .col-md-pull-10 { - right: 83.33333333%; - } - .col-md-pull-9 { - right: 75%; - } - .col-md-pull-8 { - right: 66.66666667%; - } - .col-md-pull-7 { - right: 58.33333333%; - } - .col-md-pull-6 { - right: 50%; - } - .col-md-pull-5 { - right: 41.66666667%; - } - .col-md-pull-4 { - right: 33.33333333%; - } - .col-md-pull-3 { - right: 25%; - } - .col-md-pull-2 { - right: 16.66666667%; - } - .col-md-pull-1 { - right: 8.33333333%; - } - .col-md-pull-0 { - right: auto; - } - .col-md-push-12 { - left: 100%; - } - .col-md-push-11 { - left: 91.66666667%; - } - .col-md-push-10 { - left: 83.33333333%; - } - .col-md-push-9 { - left: 75%; - } - .col-md-push-8 { - left: 66.66666667%; - } - .col-md-push-7 { - left: 58.33333333%; - } - .col-md-push-6 { - left: 50%; - } - .col-md-push-5 { - left: 41.66666667%; - } - .col-md-push-4 { - left: 33.33333333%; - } - .col-md-push-3 { - left: 25%; - } - .col-md-push-2 { - left: 16.66666667%; - } - .col-md-push-1 { - left: 8.33333333%; - } - .col-md-push-0 { - left: auto; - } - .col-md-offset-12 { - margin-left: 100%; - } - .col-md-offset-11 { - margin-left: 91.66666667%; - } - .col-md-offset-10 { - margin-left: 83.33333333%; - } - .col-md-offset-9 { - margin-left: 75%; - } - .col-md-offset-8 { - margin-left: 66.66666667%; - } - .col-md-offset-7 { - margin-left: 58.33333333%; - } - .col-md-offset-6 { - margin-left: 50%; - } - .col-md-offset-5 { - margin-left: 41.66666667%; - } - .col-md-offset-4 { - margin-left: 33.33333333%; - } - .col-md-offset-3 { - margin-left: 25%; - } - .col-md-offset-2 { - margin-left: 16.66666667%; - } - .col-md-offset-1 { - margin-left: 8.33333333%; - } - .col-md-offset-0 { - margin-left: 0%; - } -} -@media (min-width: 1200px) { - .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { - float: left; - } - .col-lg-12 { - width: 100%; - } - .col-lg-11 { - width: 91.66666667%; - } - .col-lg-10 { - width: 83.33333333%; - } - .col-lg-9 { - width: 75%; - } - .col-lg-8 { - width: 66.66666667%; - } - .col-lg-7 { - width: 58.33333333%; - } - .col-lg-6 { - width: 50%; - } - .col-lg-5 { - width: 41.66666667%; - } - .col-lg-4 { - width: 33.33333333%; - } - .col-lg-3 { - width: 25%; - } - .col-lg-2 { - width: 16.66666667%; - } - .col-lg-1 { - width: 8.33333333%; - } - .col-lg-pull-12 { - right: 100%; - } - .col-lg-pull-11 { - right: 91.66666667%; - } - .col-lg-pull-10 { - right: 83.33333333%; - } - .col-lg-pull-9 { - right: 75%; - } - .col-lg-pull-8 { - right: 66.66666667%; - } - .col-lg-pull-7 { - right: 58.33333333%; - } - .col-lg-pull-6 { - right: 50%; - } - .col-lg-pull-5 { - right: 41.66666667%; - } - .col-lg-pull-4 { - right: 33.33333333%; - } - .col-lg-pull-3 { - right: 25%; - } - .col-lg-pull-2 { - right: 16.66666667%; - } - .col-lg-pull-1 { - right: 8.33333333%; - } - .col-lg-pull-0 { - right: auto; - } - .col-lg-push-12 { - left: 100%; - } - .col-lg-push-11 { - left: 91.66666667%; - } - .col-lg-push-10 { - left: 83.33333333%; - } - .col-lg-push-9 { - left: 75%; - } - .col-lg-push-8 { - left: 66.66666667%; - } - .col-lg-push-7 { - left: 58.33333333%; - } - .col-lg-push-6 { - left: 50%; - } - .col-lg-push-5 { - left: 41.66666667%; - } - .col-lg-push-4 { - left: 33.33333333%; - } - .col-lg-push-3 { - left: 25%; - } - .col-lg-push-2 { - left: 16.66666667%; - } - .col-lg-push-1 { - left: 8.33333333%; - } - .col-lg-push-0 { - left: auto; - } - .col-lg-offset-12 { - margin-left: 100%; - } - .col-lg-offset-11 { - margin-left: 91.66666667%; - } - .col-lg-offset-10 { - margin-left: 83.33333333%; - } - .col-lg-offset-9 { - margin-left: 75%; - } - .col-lg-offset-8 { - margin-left: 66.66666667%; - } - .col-lg-offset-7 { - margin-left: 58.33333333%; - } - .col-lg-offset-6 { - margin-left: 50%; - } - .col-lg-offset-5 { - margin-left: 41.66666667%; - } - .col-lg-offset-4 { - margin-left: 33.33333333%; - } - .col-lg-offset-3 { - margin-left: 25%; - } - .col-lg-offset-2 { - margin-left: 16.66666667%; - } - .col-lg-offset-1 { - margin-left: 8.33333333%; - } - .col-lg-offset-0 { - margin-left: 0%; - } -} -table { - background-color: transparent; -} -caption { - padding-top: 8px; - padding-bottom: 8px; - color: #bbbbbb; - text-align: left; -} -th { - text-align: left; -} -.table { - width: 100%; - max-width: 100%; - margin-bottom: 23px; -} -.table > thead > tr > th, -.table > tbody > tr > th, -.table > tfoot > tr > th, -.table > thead > tr > td, -.table > tbody > tr > td, -.table > tfoot > tr > td { - padding: 8px; - line-height: 1.846; - vertical-align: top; - border-top: 1px solid #dddddd; -} -.table > thead > tr > th { - vertical-align: bottom; - border-bottom: 2px solid #dddddd; -} -.table > caption + thead > tr:first-child > th, -.table > colgroup + thead > tr:first-child > th, -.table > thead:first-child > tr:first-child > th, -.table > caption + thead > tr:first-child > td, -.table > colgroup + thead > tr:first-child > td, -.table > thead:first-child > tr:first-child > td { - border-top: 0; -} -.table > tbody + tbody { - border-top: 2px solid #dddddd; -} -.table .table { - background-color: #ffffff; -} -.table-condensed > thead > tr > th, -.table-condensed > tbody > tr > th, -.table-condensed > tfoot > tr > th, -.table-condensed > thead > tr > td, -.table-condensed > tbody > tr > td, -.table-condensed > tfoot > tr > td { - padding: 5px; -} -.table-bordered { - border: 1px solid #dddddd; -} -.table-bordered > thead > tr > th, -.table-bordered > tbody > tr > th, -.table-bordered > tfoot > tr > th, -.table-bordered > thead > tr > td, -.table-bordered > tbody > tr > td, -.table-bordered > tfoot > tr > td { - border: 1px solid #dddddd; -} -.table-bordered > thead > tr > th, -.table-bordered > thead > tr > td { - border-bottom-width: 2px; -} -.table-striped > tbody > tr:nth-of-type(odd) { - background-color: #f9f9f9; -} -.table-hover > tbody > tr:hover { - background-color: #f5f5f5; -} -table col[class*="col-"] { - position: static; - float: none; - display: table-column; -} -table td[class*="col-"], -table th[class*="col-"] { - position: static; - float: none; - display: table-cell; -} -.table > thead > tr > td.active, -.table > tbody > tr > td.active, -.table > tfoot > tr > td.active, -.table > thead > tr > th.active, -.table > tbody > tr > th.active, -.table > tfoot > tr > th.active, -.table > thead > tr.active > td, -.table > tbody > tr.active > td, -.table > tfoot > tr.active > td, -.table > thead > tr.active > th, -.table > tbody > tr.active > th, -.table > tfoot > tr.active > th { - background-color: #f5f5f5; -} -.table-hover > tbody > tr > td.active:hover, -.table-hover > tbody > tr > th.active:hover, -.table-hover > tbody > tr.active:hover > td, -.table-hover > tbody > tr:hover > .active, -.table-hover > tbody > tr.active:hover > th { - background-color: #e8e8e8; -} -.table > thead > tr > td.success, -.table > tbody > tr > td.success, -.table > tfoot > tr > td.success, -.table > thead > tr > th.success, -.table > tbody > tr > th.success, -.table > tfoot > tr > th.success, -.table > thead > tr.success > td, -.table > tbody > tr.success > td, -.table > tfoot > tr.success > td, -.table > thead > tr.success > th, -.table > tbody > tr.success > th, -.table > tfoot > tr.success > th { - background-color: #dff0d8; -} -.table-hover > tbody > tr > td.success:hover, -.table-hover > tbody > tr > th.success:hover, -.table-hover > tbody > tr.success:hover > td, -.table-hover > tbody > tr:hover > .success, -.table-hover > tbody > tr.success:hover > th { - background-color: #d0e9c6; -} -.table > thead > tr > td.info, -.table > tbody > tr > td.info, -.table > tfoot > tr > td.info, -.table > thead > tr > th.info, -.table > tbody > tr > th.info, -.table > tfoot > tr > th.info, -.table > thead > tr.info > td, -.table > tbody > tr.info > td, -.table > tfoot > tr.info > td, -.table > thead > tr.info > th, -.table > tbody > tr.info > th, -.table > tfoot > tr.info > th { - background-color: #e1bee7; -} -.table-hover > tbody > tr > td.info:hover, -.table-hover > tbody > tr > th.info:hover, -.table-hover > tbody > tr.info:hover > td, -.table-hover > tbody > tr:hover > .info, -.table-hover > tbody > tr.info:hover > th { - background-color: #d8abe0; -} -.table > thead > tr > td.warning, -.table > tbody > tr > td.warning, -.table > tfoot > tr > td.warning, -.table > thead > tr > th.warning, -.table > tbody > tr > th.warning, -.table > tfoot > tr > th.warning, -.table > thead > tr.warning > td, -.table > tbody > tr.warning > td, -.table > tfoot > tr.warning > td, -.table > thead > tr.warning > th, -.table > tbody > tr.warning > th, -.table > tfoot > tr.warning > th { - background-color: #ffe0b2; -} -.table-hover > tbody > tr > td.warning:hover, -.table-hover > tbody > tr > th.warning:hover, -.table-hover > tbody > tr.warning:hover > td, -.table-hover > tbody > tr:hover > .warning, -.table-hover > tbody > tr.warning:hover > th { - background-color: #ffd699; -} -.table > thead > tr > td.danger, -.table > tbody > tr > td.danger, -.table > tfoot > tr > td.danger, -.table > thead > tr > th.danger, -.table > tbody > tr > th.danger, -.table > tfoot > tr > th.danger, -.table > thead > tr.danger > td, -.table > tbody > tr.danger > td, -.table > tfoot > tr.danger > td, -.table > thead > tr.danger > th, -.table > tbody > tr.danger > th, -.table > tfoot > tr.danger > th { - background-color: #f9bdbb; -} -.table-hover > tbody > tr > td.danger:hover, -.table-hover > tbody > tr > th.danger:hover, -.table-hover > tbody > tr.danger:hover > td, -.table-hover > tbody > tr:hover > .danger, -.table-hover > tbody > tr.danger:hover > th { - background-color: #f7a6a4; -} -.table-responsive { - overflow-x: auto; - min-height: 0.01%; -} -@media screen and (max-width: 767px) { - .table-responsive { - width: 100%; - margin-bottom: 17.25px; - overflow-y: hidden; - -ms-overflow-style: -ms-autohiding-scrollbar; - border: 1px solid #dddddd; - } - .table-responsive > .table { - margin-bottom: 0; - } - .table-responsive > .table > thead > tr > th, - .table-responsive > .table > tbody > tr > th, - .table-responsive > .table > tfoot > tr > th, - .table-responsive > .table > thead > tr > td, - .table-responsive > .table > tbody > tr > td, - .table-responsive > .table > tfoot > tr > td { - white-space: nowrap; - } - .table-responsive > .table-bordered { - border: 0; - } - .table-responsive > .table-bordered > thead > tr > th:first-child, - .table-responsive > .table-bordered > tbody > tr > th:first-child, - .table-responsive > .table-bordered > tfoot > tr > th:first-child, - .table-responsive > .table-bordered > thead > tr > td:first-child, - .table-responsive > .table-bordered > tbody > tr > td:first-child, - .table-responsive > .table-bordered > tfoot > tr > td:first-child { - border-left: 0; - } - .table-responsive > .table-bordered > thead > tr > th:last-child, - .table-responsive > .table-bordered > tbody > tr > th:last-child, - .table-responsive > .table-bordered > tfoot > tr > th:last-child, - .table-responsive > .table-bordered > thead > tr > td:last-child, - .table-responsive > .table-bordered > tbody > tr > td:last-child, - .table-responsive > .table-bordered > tfoot > tr > td:last-child { - border-right: 0; - } - .table-responsive > .table-bordered > tbody > tr:last-child > th, - .table-responsive > .table-bordered > tfoot > tr:last-child > th, - .table-responsive > .table-bordered > tbody > tr:last-child > td, - .table-responsive > .table-bordered > tfoot > tr:last-child > td { - border-bottom: 0; - } -} -fieldset { - padding: 0; - margin: 0; - border: 0; - min-width: 0; -} -legend { - display: block; - width: 100%; - padding: 0; - margin-bottom: 23px; - font-size: 19.5px; - line-height: inherit; - color: #212121; - border: 0; - border-bottom: 1px solid #e5e5e5; -} -label { - display: inline-block; - max-width: 100%; - margin-bottom: 5px; - font-weight: bold; -} -input[type="search"] { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -input[type="radio"], -input[type="checkbox"] { - margin: 4px 0 0; - margin-top: 1px \9; - line-height: normal; -} -input[type="file"] { - display: block; -} -input[type="range"] { - display: block; - width: 100%; -} -select[multiple], -select[size] { - height: auto; -} -input[type="file"]:focus, -input[type="radio"]:focus, -input[type="checkbox"]:focus { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} -output { - display: block; - padding-top: 7px; - font-size: 13px; - line-height: 1.846; - color: #666666; -} -.form-control { - display: block; - width: 100%; - height: 37px; - padding: 6px 16px; - font-size: 13px; - line-height: 1.846; - color: #666666; - background-color: transparent; - background-image: none; - border: 1px solid transparent; - border-radius: 3px; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; - -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; - transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; -} -.form-control:focus { - border-color: #66afe9; - outline: 0; - -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6); - box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6); -} -.form-control::-moz-placeholder { - color: #bbbbbb; - opacity: 1; -} -.form-control:-ms-input-placeholder { - color: #bbbbbb; -} -.form-control::-webkit-input-placeholder { - color: #bbbbbb; -} -.form-control::-ms-expand { - border: 0; - background-color: transparent; -} -.form-control[disabled], -.form-control[readonly], -fieldset[disabled] .form-control { - background-color: transparent; - opacity: 1; -} -.form-control[disabled], -fieldset[disabled] .form-control { - cursor: not-allowed; -} -textarea.form-control { - height: auto; -} -input[type="search"] { - -webkit-appearance: none; -} -@media screen and (-webkit-min-device-pixel-ratio: 0) { - input[type="date"].form-control, - input[type="time"].form-control, - input[type="datetime-local"].form-control, - input[type="month"].form-control { - line-height: 37px; - } - input[type="date"].input-sm, - input[type="time"].input-sm, - input[type="datetime-local"].input-sm, - input[type="month"].input-sm, - .input-group-sm input[type="date"], - .input-group-sm input[type="time"], - .input-group-sm input[type="datetime-local"], - .input-group-sm input[type="month"] { - line-height: 30px; - } - input[type="date"].input-lg, - input[type="time"].input-lg, - input[type="datetime-local"].input-lg, - input[type="month"].input-lg, - .input-group-lg input[type="date"], - .input-group-lg input[type="time"], - .input-group-lg input[type="datetime-local"], - .input-group-lg input[type="month"] { - line-height: 45px; - } -} -.form-group { - margin-bottom: 15px; -} -.radio, -.checkbox { - position: relative; - display: block; - margin-top: 10px; - margin-bottom: 10px; -} -.radio label, -.checkbox label { - min-height: 23px; - padding-left: 20px; - margin-bottom: 0; - font-weight: normal; - cursor: pointer; -} -.radio input[type="radio"], -.radio-inline input[type="radio"], -.checkbox input[type="checkbox"], -.checkbox-inline input[type="checkbox"] { - position: absolute; - margin-left: -20px; - margin-top: 4px \9; -} -.radio + .radio, -.checkbox + .checkbox { - margin-top: -5px; -} -.radio-inline, -.checkbox-inline { - position: relative; - display: inline-block; - padding-left: 20px; - margin-bottom: 0; - vertical-align: middle; - font-weight: normal; - cursor: pointer; -} -.radio-inline + .radio-inline, -.checkbox-inline + .checkbox-inline { - margin-top: 0; - margin-left: 10px; -} -input[type="radio"][disabled], -input[type="checkbox"][disabled], -input[type="radio"].disabled, -input[type="checkbox"].disabled, -fieldset[disabled] input[type="radio"], -fieldset[disabled] input[type="checkbox"] { - cursor: not-allowed; -} -.radio-inline.disabled, -.checkbox-inline.disabled, -fieldset[disabled] .radio-inline, -fieldset[disabled] .checkbox-inline { - cursor: not-allowed; -} -.radio.disabled label, -.checkbox.disabled label, -fieldset[disabled] .radio label, -fieldset[disabled] .checkbox label { - cursor: not-allowed; -} -.form-control-static { - padding-top: 7px; - padding-bottom: 7px; - margin-bottom: 0; - min-height: 36px; -} -.form-control-static.input-lg, -.form-control-static.input-sm { - padding-left: 0; - padding-right: 0; -} -.input-sm { - height: 30px; - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; -} -select.input-sm { - height: 30px; - line-height: 30px; -} -textarea.input-sm, -select[multiple].input-sm { - height: auto; -} -.form-group-sm .form-control { - height: 30px; - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; -} -.form-group-sm select.form-control { - height: 30px; - line-height: 30px; -} -.form-group-sm textarea.form-control, -.form-group-sm select[multiple].form-control { - height: auto; -} -.form-group-sm .form-control-static { - height: 30px; - min-height: 35px; - padding: 6px 10px; - font-size: 12px; - line-height: 1.5; -} -.input-lg { - height: 45px; - padding: 10px 16px; - font-size: 17px; - line-height: 1.3333333; - border-radius: 3px; -} -select.input-lg { - height: 45px; - line-height: 45px; -} -textarea.input-lg, -select[multiple].input-lg { - height: auto; -} -.form-group-lg .form-control { - height: 45px; - padding: 10px 16px; - font-size: 17px; - line-height: 1.3333333; - border-radius: 3px; -} -.form-group-lg select.form-control { - height: 45px; - line-height: 45px; -} -.form-group-lg textarea.form-control, -.form-group-lg select[multiple].form-control { - height: auto; -} -.form-group-lg .form-control-static { - height: 45px; - min-height: 40px; - padding: 11px 16px; - font-size: 17px; - line-height: 1.3333333; -} -.has-feedback { - position: relative; -} -.has-feedback .form-control { - padding-right: 46.25px; -} -.form-control-feedback { - position: absolute; - top: 0; - right: 0; - z-index: 2; - display: block; - width: 37px; - height: 37px; - line-height: 37px; - text-align: center; - pointer-events: none; -} -.input-lg + .form-control-feedback, -.input-group-lg + .form-control-feedback, -.form-group-lg .form-control + .form-control-feedback { - width: 45px; - height: 45px; - line-height: 45px; -} -.input-sm + .form-control-feedback, -.input-group-sm + .form-control-feedback, -.form-group-sm .form-control + .form-control-feedback { - width: 30px; - height: 30px; - line-height: 30px; -} -.has-success .help-block, -.has-success .control-label, -.has-success .radio, -.has-success .checkbox, -.has-success .radio-inline, -.has-success .checkbox-inline, -.has-success.radio label, -.has-success.checkbox label, -.has-success.radio-inline label, -.has-success.checkbox-inline label { - color: #4caf50; -} -.has-success .form-control { - border-color: #4caf50; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} -.has-success .form-control:focus { - border-color: #3d8b40; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #92cf94; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #92cf94; -} -.has-success .input-group-addon { - color: #4caf50; - border-color: #4caf50; - background-color: #dff0d8; -} -.has-success .form-control-feedback { - color: #4caf50; -} -.has-warning .help-block, -.has-warning .control-label, -.has-warning .radio, -.has-warning .checkbox, -.has-warning .radio-inline, -.has-warning .checkbox-inline, -.has-warning.radio label, -.has-warning.checkbox label, -.has-warning.radio-inline label, -.has-warning.checkbox-inline label { - color: #ff9800; -} -.has-warning .form-control { - border-color: #ff9800; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} -.has-warning .form-control:focus { - border-color: #cc7a00; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ffc166; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ffc166; -} -.has-warning .input-group-addon { - color: #ff9800; - border-color: #ff9800; - background-color: #ffe0b2; -} -.has-warning .form-control-feedback { - color: #ff9800; -} -.has-error .help-block, -.has-error .control-label, -.has-error .radio, -.has-error .checkbox, -.has-error .radio-inline, -.has-error .checkbox-inline, -.has-error.radio label, -.has-error.checkbox label, -.has-error.radio-inline label, -.has-error.checkbox-inline label { - color: #e51c23; -} -.has-error .form-control { - border-color: #e51c23; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); -} -.has-error .form-control:focus { - border-color: #b9151b; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ef787c; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ef787c; -} -.has-error .input-group-addon { - color: #e51c23; - border-color: #e51c23; - background-color: #f9bdbb; -} -.has-error .form-control-feedback { - color: #e51c23; -} -.has-feedback label ~ .form-control-feedback { - top: 28px; -} -.has-feedback label.sr-only ~ .form-control-feedback { - top: 0; -} -.help-block { - display: block; - margin-top: 5px; - margin-bottom: 10px; - color: #a6a6a6; -} -@media (min-width: 768px) { - .form-inline .form-group { - display: inline-block; - margin-bottom: 0; - vertical-align: middle; - } - .form-inline .form-control { - display: inline-block; - width: auto; - vertical-align: middle; - } - .form-inline .form-control-static { - display: inline-block; - } - .form-inline .input-group { - display: inline-table; - vertical-align: middle; - } - .form-inline .input-group .input-group-addon, - .form-inline .input-group .input-group-btn, - .form-inline .input-group .form-control { - width: auto; - } - .form-inline .input-group > .form-control { - width: 100%; - } - .form-inline .control-label { - margin-bottom: 0; - vertical-align: middle; - } - .form-inline .radio, - .form-inline .checkbox { - display: inline-block; - margin-top: 0; - margin-bottom: 0; - vertical-align: middle; - } - .form-inline .radio label, - .form-inline .checkbox label { - padding-left: 0; - } - .form-inline .radio input[type="radio"], - .form-inline .checkbox input[type="checkbox"] { - position: relative; - margin-left: 0; - } - .form-inline .has-feedback .form-control-feedback { - top: 0; - } -} -.form-horizontal .radio, -.form-horizontal .checkbox, -.form-horizontal .radio-inline, -.form-horizontal .checkbox-inline { - margin-top: 0; - margin-bottom: 0; - padding-top: 7px; -} -.form-horizontal .radio, -.form-horizontal .checkbox { - min-height: 30px; -} -.form-horizontal .form-group { - margin-left: -15px; - margin-right: -15px; -} -@media (min-width: 768px) { - .form-horizontal .control-label { - text-align: right; - margin-bottom: 0; - padding-top: 7px; - } -} -.form-horizontal .has-feedback .form-control-feedback { - right: 15px; -} -@media (min-width: 768px) { - .form-horizontal .form-group-lg .control-label { - padding-top: 11px; - font-size: 17px; - } -} -@media (min-width: 768px) { - .form-horizontal .form-group-sm .control-label { - padding-top: 6px; - font-size: 12px; - } -} -.btn { - display: inline-block; - margin-bottom: 0; - font-weight: normal; - text-align: center; - vertical-align: middle; - -ms-touch-action: manipulation; - touch-action: manipulation; - cursor: pointer; - background-image: none; - border: 1px solid transparent; - white-space: nowrap; - padding: 6px 16px; - font-size: 13px; - line-height: 1.846; - border-radius: 3px; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} -.btn:focus, -.btn:active:focus, -.btn.active:focus, -.btn.focus, -.btn:active.focus, -.btn.active.focus { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} -.btn:hover, -.btn:focus, -.btn.focus { - color: #444444; - text-decoration: none; -} -.btn:active, -.btn.active { - outline: 0; - background-image: none; - -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); -} -.btn.disabled, -.btn[disabled], -fieldset[disabled] .btn { - cursor: not-allowed; - opacity: 0.65; - filter: alpha(opacity=65); - -webkit-box-shadow: none; - box-shadow: none; -} -a.btn.disabled, -fieldset[disabled] a.btn { - pointer-events: none; -} -.btn-default { - color: #444444; - background-color: #ffffff; - border-color: transparent; -} -.btn-default:focus, -.btn-default.focus { - color: #444444; - background-color: #e6e6e6; - border-color: rgba(0, 0, 0, 0); -} -.btn-default:hover { - color: #444444; - background-color: #e6e6e6; - border-color: rgba(0, 0, 0, 0); -} -.btn-default:active, -.btn-default.active, -.open > .dropdown-toggle.btn-default { - color: #444444; - background-color: #e6e6e6; - border-color: rgba(0, 0, 0, 0); -} -.btn-default:active:hover, -.btn-default.active:hover, -.open > .dropdown-toggle.btn-default:hover, -.btn-default:active:focus, -.btn-default.active:focus, -.open > .dropdown-toggle.btn-default:focus, -.btn-default:active.focus, -.btn-default.active.focus, -.open > .dropdown-toggle.btn-default.focus { - color: #444444; - background-color: #d4d4d4; - border-color: rgba(0, 0, 0, 0); -} -.btn-default:active, -.btn-default.active, -.open > .dropdown-toggle.btn-default { - background-image: none; -} -.btn-default.disabled:hover, -.btn-default[disabled]:hover, -fieldset[disabled] .btn-default:hover, -.btn-default.disabled:focus, -.btn-default[disabled]:focus, -fieldset[disabled] .btn-default:focus, -.btn-default.disabled.focus, -.btn-default[disabled].focus, -fieldset[disabled] .btn-default.focus { - background-color: #ffffff; - border-color: transparent; -} -.btn-default .badge { - color: #ffffff; - background-color: #444444; -} -.btn-primary { - color: #ffffff; - background-color: #2196f3; - border-color: transparent; -} -.btn-primary:focus, -.btn-primary.focus { - color: #ffffff; - background-color: #0c7cd5; - border-color: rgba(0, 0, 0, 0); -} -.btn-primary:hover { - color: #ffffff; - background-color: #0c7cd5; - border-color: rgba(0, 0, 0, 0); -} -.btn-primary:active, -.btn-primary.active, -.open > .dropdown-toggle.btn-primary { - color: #ffffff; - background-color: #0c7cd5; - border-color: rgba(0, 0, 0, 0); -} -.btn-primary:active:hover, -.btn-primary.active:hover, -.open > .dropdown-toggle.btn-primary:hover, -.btn-primary:active:focus, -.btn-primary.active:focus, -.open > .dropdown-toggle.btn-primary:focus, -.btn-primary:active.focus, -.btn-primary.active.focus, -.open > .dropdown-toggle.btn-primary.focus { - color: #ffffff; - background-color: #0a68b4; - border-color: rgba(0, 0, 0, 0); -} -.btn-primary:active, -.btn-primary.active, -.open > .dropdown-toggle.btn-primary { - background-image: none; -} -.btn-primary.disabled:hover, -.btn-primary[disabled]:hover, -fieldset[disabled] .btn-primary:hover, -.btn-primary.disabled:focus, -.btn-primary[disabled]:focus, -fieldset[disabled] .btn-primary:focus, -.btn-primary.disabled.focus, -.btn-primary[disabled].focus, -fieldset[disabled] .btn-primary.focus { - background-color: #2196f3; - border-color: transparent; -} -.btn-primary .badge { - color: #2196f3; - background-color: #ffffff; -} -.btn-success { - color: #ffffff; - background-color: #4caf50; - border-color: transparent; -} -.btn-success:focus, -.btn-success.focus { - color: #ffffff; - background-color: #3d8b40; - border-color: rgba(0, 0, 0, 0); -} -.btn-success:hover { - color: #ffffff; - background-color: #3d8b40; - border-color: rgba(0, 0, 0, 0); -} -.btn-success:active, -.btn-success.active, -.open > .dropdown-toggle.btn-success { - color: #ffffff; - background-color: #3d8b40; - border-color: rgba(0, 0, 0, 0); -} -.btn-success:active:hover, -.btn-success.active:hover, -.open > .dropdown-toggle.btn-success:hover, -.btn-success:active:focus, -.btn-success.active:focus, -.open > .dropdown-toggle.btn-success:focus, -.btn-success:active.focus, -.btn-success.active.focus, -.open > .dropdown-toggle.btn-success.focus { - color: #ffffff; - background-color: #327334; - border-color: rgba(0, 0, 0, 0); -} -.btn-success:active, -.btn-success.active, -.open > .dropdown-toggle.btn-success { - background-image: none; -} -.btn-success.disabled:hover, -.btn-success[disabled]:hover, -fieldset[disabled] .btn-success:hover, -.btn-success.disabled:focus, -.btn-success[disabled]:focus, -fieldset[disabled] .btn-success:focus, -.btn-success.disabled.focus, -.btn-success[disabled].focus, -fieldset[disabled] .btn-success.focus { - background-color: #4caf50; - border-color: transparent; -} -.btn-success .badge { - color: #4caf50; - background-color: #ffffff; -} -.btn-info { - color: #ffffff; - background-color: #9c27b0; - border-color: transparent; -} -.btn-info:focus, -.btn-info.focus { - color: #ffffff; - background-color: #771e86; - border-color: rgba(0, 0, 0, 0); -} -.btn-info:hover { - color: #ffffff; - background-color: #771e86; - border-color: rgba(0, 0, 0, 0); -} -.btn-info:active, -.btn-info.active, -.open > .dropdown-toggle.btn-info { - color: #ffffff; - background-color: #771e86; - border-color: rgba(0, 0, 0, 0); -} -.btn-info:active:hover, -.btn-info.active:hover, -.open > .dropdown-toggle.btn-info:hover, -.btn-info:active:focus, -.btn-info.active:focus, -.open > .dropdown-toggle.btn-info:focus, -.btn-info:active.focus, -.btn-info.active.focus, -.open > .dropdown-toggle.btn-info.focus { - color: #ffffff; - background-color: #5d1769; - border-color: rgba(0, 0, 0, 0); -} -.btn-info:active, -.btn-info.active, -.open > .dropdown-toggle.btn-info { - background-image: none; -} -.btn-info.disabled:hover, -.btn-info[disabled]:hover, -fieldset[disabled] .btn-info:hover, -.btn-info.disabled:focus, -.btn-info[disabled]:focus, -fieldset[disabled] .btn-info:focus, -.btn-info.disabled.focus, -.btn-info[disabled].focus, -fieldset[disabled] .btn-info.focus { - background-color: #9c27b0; - border-color: transparent; -} -.btn-info .badge { - color: #9c27b0; - background-color: #ffffff; -} -.btn-warning { - color: #ffffff; - background-color: #ff9800; - border-color: transparent; -} -.btn-warning:focus, -.btn-warning.focus { - color: #ffffff; - background-color: #cc7a00; - border-color: rgba(0, 0, 0, 0); -} -.btn-warning:hover { - color: #ffffff; - background-color: #cc7a00; - border-color: rgba(0, 0, 0, 0); -} -.btn-warning:active, -.btn-warning.active, -.open > .dropdown-toggle.btn-warning { - color: #ffffff; - background-color: #cc7a00; - border-color: rgba(0, 0, 0, 0); -} -.btn-warning:active:hover, -.btn-warning.active:hover, -.open > .dropdown-toggle.btn-warning:hover, -.btn-warning:active:focus, -.btn-warning.active:focus, -.open > .dropdown-toggle.btn-warning:focus, -.btn-warning:active.focus, -.btn-warning.active.focus, -.open > .dropdown-toggle.btn-warning.focus { - color: #ffffff; - background-color: #a86400; - border-color: rgba(0, 0, 0, 0); -} -.btn-warning:active, -.btn-warning.active, -.open > .dropdown-toggle.btn-warning { - background-image: none; -} -.btn-warning.disabled:hover, -.btn-warning[disabled]:hover, -fieldset[disabled] .btn-warning:hover, -.btn-warning.disabled:focus, -.btn-warning[disabled]:focus, -fieldset[disabled] .btn-warning:focus, -.btn-warning.disabled.focus, -.btn-warning[disabled].focus, -fieldset[disabled] .btn-warning.focus { - background-color: #ff9800; - border-color: transparent; -} -.btn-warning .badge { - color: #ff9800; - background-color: #ffffff; -} -.btn-danger { - color: #ffffff; - background-color: #e51c23; - border-color: transparent; -} -.btn-danger:focus, -.btn-danger.focus { - color: #ffffff; - background-color: #b9151b; - border-color: rgba(0, 0, 0, 0); -} -.btn-danger:hover { - color: #ffffff; - background-color: #b9151b; - border-color: rgba(0, 0, 0, 0); -} -.btn-danger:active, -.btn-danger.active, -.open > .dropdown-toggle.btn-danger { - color: #ffffff; - background-color: #b9151b; - border-color: rgba(0, 0, 0, 0); -} -.btn-danger:active:hover, -.btn-danger.active:hover, -.open > .dropdown-toggle.btn-danger:hover, -.btn-danger:active:focus, -.btn-danger.active:focus, -.open > .dropdown-toggle.btn-danger:focus, -.btn-danger:active.focus, -.btn-danger.active.focus, -.open > .dropdown-toggle.btn-danger.focus { - color: #ffffff; - background-color: #991216; - border-color: rgba(0, 0, 0, 0); -} -.btn-danger:active, -.btn-danger.active, -.open > .dropdown-toggle.btn-danger { - background-image: none; -} -.btn-danger.disabled:hover, -.btn-danger[disabled]:hover, -fieldset[disabled] .btn-danger:hover, -.btn-danger.disabled:focus, -.btn-danger[disabled]:focus, -fieldset[disabled] .btn-danger:focus, -.btn-danger.disabled.focus, -.btn-danger[disabled].focus, -fieldset[disabled] .btn-danger.focus { - background-color: #e51c23; - border-color: transparent; -} -.btn-danger .badge { - color: #e51c23; - background-color: #ffffff; -} -.btn-link { - color: #2196f3; - font-weight: normal; - border-radius: 0; -} -.btn-link, -.btn-link:active, -.btn-link.active, -.btn-link[disabled], -fieldset[disabled] .btn-link { - background-color: transparent; - -webkit-box-shadow: none; - box-shadow: none; -} -.btn-link, -.btn-link:hover, -.btn-link:focus, -.btn-link:active { - border-color: transparent; -} -.btn-link:hover, -.btn-link:focus { - color: #0a6ebd; - text-decoration: underline; - background-color: transparent; -} -.btn-link[disabled]:hover, -fieldset[disabled] .btn-link:hover, -.btn-link[disabled]:focus, -fieldset[disabled] .btn-link:focus { - color: #bbbbbb; - text-decoration: none; -} -.btn-lg, -.btn-group-lg > .btn { - padding: 10px 16px; - font-size: 17px; - line-height: 1.3333333; - border-radius: 3px; -} -.btn-sm, -.btn-group-sm > .btn { - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; -} -.btn-xs, -.btn-group-xs > .btn { - padding: 1px 5px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; -} -.btn-block { - display: block; - width: 100%; -} -.btn-block + .btn-block { - margin-top: 5px; -} -input[type="submit"].btn-block, -input[type="reset"].btn-block, -input[type="button"].btn-block { - width: 100%; -} -.fade { - opacity: 0; - -webkit-transition: opacity 0.15s linear; - -o-transition: opacity 0.15s linear; - transition: opacity 0.15s linear; -} -.fade.in { - opacity: 1; -} -.collapse { - display: none; -} -.collapse.in { - display: block; -} -tr.collapse.in { - display: table-row; -} -tbody.collapse.in { - display: table-row-group; -} -.collapsing { - position: relative; - height: 0; - overflow: hidden; - -webkit-transition-property: height, visibility; - -o-transition-property: height, visibility; - transition-property: height, visibility; - -webkit-transition-duration: 0.35s; - -o-transition-duration: 0.35s; - transition-duration: 0.35s; - -webkit-transition-timing-function: ease; - -o-transition-timing-function: ease; - transition-timing-function: ease; -} -.caret { - display: inline-block; - width: 0; - height: 0; - margin-left: 2px; - vertical-align: middle; - border-top: 4px dashed; - border-top: 4px solid \9; - border-right: 4px solid transparent; - border-left: 4px solid transparent; -} -.dropup, -.dropdown { - position: relative; -} -.dropdown-toggle:focus { - outline: 0; -} -.dropdown-menu { - position: absolute; - top: 100%; - left: 0; - z-index: 1000; - display: none; - float: left; - min-width: 160px; - padding: 5px 0; - margin: 2px 0 0; - list-style: none; - font-size: 13px; - text-align: left; - background-color: #ffffff; - border: 1px solid #cccccc; - border: 1px solid rgba(0, 0, 0, 0.15); - border-radius: 3px; - -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); - box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); - -webkit-background-clip: padding-box; - background-clip: padding-box; -} -.dropdown-menu.pull-right { - right: 0; - left: auto; -} -.dropdown-menu .divider { - height: 1px; - margin: 10.5px 0; - overflow: hidden; - background-color: #e5e5e5; -} -.dropdown-menu > li > a { - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: 1.846; - color: #666666; - white-space: nowrap; -} -.dropdown-menu > li > a:hover, -.dropdown-menu > li > a:focus { - text-decoration: none; - color: #141414; - background-color: #eeeeee; -} -.dropdown-menu > .active > a, -.dropdown-menu > .active > a:hover, -.dropdown-menu > .active > a:focus { - color: #ffffff; - text-decoration: none; - outline: 0; - background-color: #2196f3; -} -.dropdown-menu > .disabled > a, -.dropdown-menu > .disabled > a:hover, -.dropdown-menu > .disabled > a:focus { - color: #bbbbbb; -} -.dropdown-menu > .disabled > a:hover, -.dropdown-menu > .disabled > a:focus { - text-decoration: none; - background-color: transparent; - background-image: none; - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - cursor: not-allowed; -} -.open > .dropdown-menu { - display: block; -} -.open > a { - outline: 0; -} -.dropdown-menu-right { - left: auto; - right: 0; -} -.dropdown-menu-left { - left: 0; - right: auto; -} -.dropdown-header { - display: block; - padding: 3px 20px; - font-size: 12px; - line-height: 1.846; - color: #bbbbbb; - white-space: nowrap; -} -.dropdown-backdrop { - position: fixed; - left: 0; - right: 0; - bottom: 0; - top: 0; - z-index: 990; -} -.pull-right > .dropdown-menu { - right: 0; - left: auto; -} -.dropup .caret, -.navbar-fixed-bottom .dropdown .caret { - border-top: 0; - border-bottom: 4px dashed; - border-bottom: 4px solid \9; - content: ""; -} -.dropup .dropdown-menu, -.navbar-fixed-bottom .dropdown .dropdown-menu { - top: auto; - bottom: 100%; - margin-bottom: 2px; -} -@media (min-width: 768px) { - .navbar-right .dropdown-menu { - left: auto; - right: 0; - } - .navbar-right .dropdown-menu-left { - left: 0; - right: auto; - } -} -.btn-group, -.btn-group-vertical { - position: relative; - display: inline-block; - vertical-align: middle; -} -.btn-group > .btn, -.btn-group-vertical > .btn { - position: relative; - float: left; -} -.btn-group > .btn:hover, -.btn-group-vertical > .btn:hover, -.btn-group > .btn:focus, -.btn-group-vertical > .btn:focus, -.btn-group > .btn:active, -.btn-group-vertical > .btn:active, -.btn-group > .btn.active, -.btn-group-vertical > .btn.active { - z-index: 2; -} -.btn-group .btn + .btn, -.btn-group .btn + .btn-group, -.btn-group .btn-group + .btn, -.btn-group .btn-group + .btn-group { - margin-left: -1px; -} -.btn-toolbar { - margin-left: -5px; -} -.btn-toolbar .btn, -.btn-toolbar .btn-group, -.btn-toolbar .input-group { - float: left; -} -.btn-toolbar > .btn, -.btn-toolbar > .btn-group, -.btn-toolbar > .input-group { - margin-left: 5px; -} -.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { - border-radius: 0; -} -.btn-group > .btn:first-child { - margin-left: 0; -} -.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { - border-bottom-right-radius: 0; - border-top-right-radius: 0; -} -.btn-group > .btn:last-child:not(:first-child), -.btn-group > .dropdown-toggle:not(:first-child) { - border-bottom-left-radius: 0; - border-top-left-radius: 0; -} -.btn-group > .btn-group { - float: left; -} -.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { - border-radius: 0; -} -.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, -.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { - border-bottom-right-radius: 0; - border-top-right-radius: 0; -} -.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { - border-bottom-left-radius: 0; - border-top-left-radius: 0; -} -.btn-group .dropdown-toggle:active, -.btn-group.open .dropdown-toggle { - outline: 0; -} -.btn-group > .btn + .dropdown-toggle { - padding-left: 8px; - padding-right: 8px; -} -.btn-group > .btn-lg + .dropdown-toggle { - padding-left: 12px; - padding-right: 12px; -} -.btn-group.open .dropdown-toggle { - -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); - box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); -} -.btn-group.open .dropdown-toggle.btn-link { - -webkit-box-shadow: none; - box-shadow: none; -} -.btn .caret { - margin-left: 0; -} -.btn-lg .caret { - border-width: 5px 5px 0; - border-bottom-width: 0; -} -.dropup .btn-lg .caret { - border-width: 0 5px 5px; -} -.btn-group-vertical > .btn, -.btn-group-vertical > .btn-group, -.btn-group-vertical > .btn-group > .btn { - display: block; - float: none; - width: 100%; - max-width: 100%; -} -.btn-group-vertical > .btn-group > .btn { - float: none; -} -.btn-group-vertical > .btn + .btn, -.btn-group-vertical > .btn + .btn-group, -.btn-group-vertical > .btn-group + .btn, -.btn-group-vertical > .btn-group + .btn-group { - margin-top: -1px; - margin-left: 0; -} -.btn-group-vertical > .btn:not(:first-child):not(:last-child) { - border-radius: 0; -} -.btn-group-vertical > .btn:first-child:not(:last-child) { - border-top-right-radius: 3px; - border-top-left-radius: 3px; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; -} -.btn-group-vertical > .btn:last-child:not(:first-child) { - border-top-right-radius: 0; - border-top-left-radius: 0; - border-bottom-right-radius: 3px; - border-bottom-left-radius: 3px; -} -.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { - border-radius: 0; -} -.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, -.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; -} -.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { - border-top-right-radius: 0; - border-top-left-radius: 0; -} -.btn-group-justified { - display: table; - width: 100%; - table-layout: fixed; - border-collapse: separate; -} -.btn-group-justified > .btn, -.btn-group-justified > .btn-group { - float: none; - display: table-cell; - width: 1%; -} -.btn-group-justified > .btn-group .btn { - width: 100%; -} -.btn-group-justified > .btn-group .dropdown-menu { - left: auto; -} -[data-toggle="buttons"] > .btn input[type="radio"], -[data-toggle="buttons"] > .btn-group > .btn input[type="radio"], -[data-toggle="buttons"] > .btn input[type="checkbox"], -[data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] { - position: absolute; - clip: rect(0, 0, 0, 0); - pointer-events: none; -} -.input-group { - position: relative; - display: table; - border-collapse: separate; -} -.input-group[class*="col-"] { - float: none; - padding-left: 0; - padding-right: 0; -} -.input-group .form-control { - position: relative; - z-index: 2; - float: left; - width: 100%; - margin-bottom: 0; -} -.input-group .form-control:focus { - z-index: 3; -} -.input-group-lg > .form-control, -.input-group-lg > .input-group-addon, -.input-group-lg > .input-group-btn > .btn { - height: 45px; - padding: 10px 16px; - font-size: 17px; - line-height: 1.3333333; - border-radius: 3px; -} -select.input-group-lg > .form-control, -select.input-group-lg > .input-group-addon, -select.input-group-lg > .input-group-btn > .btn { - height: 45px; - line-height: 45px; -} -textarea.input-group-lg > .form-control, -textarea.input-group-lg > .input-group-addon, -textarea.input-group-lg > .input-group-btn > .btn, -select[multiple].input-group-lg > .form-control, -select[multiple].input-group-lg > .input-group-addon, -select[multiple].input-group-lg > .input-group-btn > .btn { - height: auto; -} -.input-group-sm > .form-control, -.input-group-sm > .input-group-addon, -.input-group-sm > .input-group-btn > .btn { - height: 30px; - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; -} -select.input-group-sm > .form-control, -select.input-group-sm > .input-group-addon, -select.input-group-sm > .input-group-btn > .btn { - height: 30px; - line-height: 30px; -} -textarea.input-group-sm > .form-control, -textarea.input-group-sm > .input-group-addon, -textarea.input-group-sm > .input-group-btn > .btn, -select[multiple].input-group-sm > .form-control, -select[multiple].input-group-sm > .input-group-addon, -select[multiple].input-group-sm > .input-group-btn > .btn { - height: auto; -} -.input-group-addon, -.input-group-btn, -.input-group .form-control { - display: table-cell; -} -.input-group-addon:not(:first-child):not(:last-child), -.input-group-btn:not(:first-child):not(:last-child), -.input-group .form-control:not(:first-child):not(:last-child) { - border-radius: 0; -} -.input-group-addon, -.input-group-btn { - width: 1%; - white-space: nowrap; - vertical-align: middle; -} -.input-group-addon { - padding: 6px 16px; - font-size: 13px; - font-weight: normal; - line-height: 1; - color: #666666; - text-align: center; - background-color: transparent; - border: 1px solid transparent; - border-radius: 3px; -} -.input-group-addon.input-sm { - padding: 5px 10px; - font-size: 12px; - border-radius: 3px; -} -.input-group-addon.input-lg { - padding: 10px 16px; - font-size: 17px; - border-radius: 3px; -} -.input-group-addon input[type="radio"], -.input-group-addon input[type="checkbox"] { - margin-top: 0; -} -.input-group .form-control:first-child, -.input-group-addon:first-child, -.input-group-btn:first-child > .btn, -.input-group-btn:first-child > .btn-group > .btn, -.input-group-btn:first-child > .dropdown-toggle, -.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), -.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { - border-bottom-right-radius: 0; - border-top-right-radius: 0; -} -.input-group-addon:first-child { - border-right: 0; -} -.input-group .form-control:last-child, -.input-group-addon:last-child, -.input-group-btn:last-child > .btn, -.input-group-btn:last-child > .btn-group > .btn, -.input-group-btn:last-child > .dropdown-toggle, -.input-group-btn:first-child > .btn:not(:first-child), -.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { - border-bottom-left-radius: 0; - border-top-left-radius: 0; -} -.input-group-addon:last-child { - border-left: 0; -} -.input-group-btn { - position: relative; - font-size: 0; - white-space: nowrap; -} -.input-group-btn > .btn { - position: relative; -} -.input-group-btn > .btn + .btn { - margin-left: -1px; -} -.input-group-btn > .btn:hover, -.input-group-btn > .btn:focus, -.input-group-btn > .btn:active { - z-index: 2; -} -.input-group-btn:first-child > .btn, -.input-group-btn:first-child > .btn-group { - margin-right: -1px; -} -.input-group-btn:last-child > .btn, -.input-group-btn:last-child > .btn-group { - z-index: 2; - margin-left: -1px; -} -.nav { - margin-bottom: 0; - padding-left: 0; - list-style: none; -} -.nav > li { - position: relative; - display: block; -} -.nav > li > a { - position: relative; - display: block; - padding: 10px 15px; -} -.nav > li > a:hover, -.nav > li > a:focus { - text-decoration: none; - background-color: #eeeeee; -} -.nav > li.disabled > a { - color: #bbbbbb; -} -.nav > li.disabled > a:hover, -.nav > li.disabled > a:focus { - color: #bbbbbb; - text-decoration: none; - background-color: transparent; - cursor: not-allowed; -} -.nav .open > a, -.nav .open > a:hover, -.nav .open > a:focus { - background-color: #eeeeee; - border-color: #2196f3; -} -.nav .nav-divider { - height: 1px; - margin: 10.5px 0; - overflow: hidden; - background-color: #e5e5e5; -} -.nav > li > a > img { - max-width: none; -} -.nav-tabs { - border-bottom: 1px solid transparent; -} -.nav-tabs > li { - float: left; - margin-bottom: -1px; -} -.nav-tabs > li > a { - margin-right: 2px; - line-height: 1.846; - border: 1px solid transparent; - border-radius: 3px 3px 0 0; -} -.nav-tabs > li > a:hover { - border-color: #eeeeee #eeeeee transparent; -} -.nav-tabs > li.active > a, -.nav-tabs > li.active > a:hover, -.nav-tabs > li.active > a:focus { - color: #666666; - background-color: transparent; - border: 1px solid transparent; - border-bottom-color: transparent; - cursor: default; -} -.nav-tabs.nav-justified { - width: 100%; - border-bottom: 0; -} -.nav-tabs.nav-justified > li { - float: none; -} -.nav-tabs.nav-justified > li > a { - text-align: center; - margin-bottom: 5px; -} -.nav-tabs.nav-justified > .dropdown .dropdown-menu { - top: auto; - left: auto; -} -@media (min-width: 768px) { - .nav-tabs.nav-justified > li { - display: table-cell; - width: 1%; - } - .nav-tabs.nav-justified > li > a { - margin-bottom: 0; - } -} -.nav-tabs.nav-justified > li > a { - margin-right: 0; - border-radius: 3px; -} -.nav-tabs.nav-justified > .active > a, -.nav-tabs.nav-justified > .active > a:hover, -.nav-tabs.nav-justified > .active > a:focus { - border: 1px solid transparent; -} -@media (min-width: 768px) { - .nav-tabs.nav-justified > li > a { - border-bottom: 1px solid transparent; - border-radius: 3px 3px 0 0; - } - .nav-tabs.nav-justified > .active > a, - .nav-tabs.nav-justified > .active > a:hover, - .nav-tabs.nav-justified > .active > a:focus { - border-bottom-color: #ffffff; - } -} -.nav-pills > li { - float: left; -} -.nav-pills > li > a { - border-radius: 3px; -} -.nav-pills > li + li { - margin-left: 2px; -} -.nav-pills > li.active > a, -.nav-pills > li.active > a:hover, -.nav-pills > li.active > a:focus { - color: #ffffff; - background-color: #2196f3; -} -.nav-stacked > li { - float: none; -} -.nav-stacked > li + li { - margin-top: 2px; - margin-left: 0; -} -.nav-justified { - width: 100%; -} -.nav-justified > li { - float: none; -} -.nav-justified > li > a { - text-align: center; - margin-bottom: 5px; -} -.nav-justified > .dropdown .dropdown-menu { - top: auto; - left: auto; -} -@media (min-width: 768px) { - .nav-justified > li { - display: table-cell; - width: 1%; - } - .nav-justified > li > a { - margin-bottom: 0; - } -} -.nav-tabs-justified { - border-bottom: 0; -} -.nav-tabs-justified > li > a { - margin-right: 0; - border-radius: 3px; -} -.nav-tabs-justified > .active > a, -.nav-tabs-justified > .active > a:hover, -.nav-tabs-justified > .active > a:focus { - border: 1px solid transparent; -} -@media (min-width: 768px) { - .nav-tabs-justified > li > a { - border-bottom: 1px solid transparent; - border-radius: 3px 3px 0 0; - } - .nav-tabs-justified > .active > a, - .nav-tabs-justified > .active > a:hover, - .nav-tabs-justified > .active > a:focus { - border-bottom-color: #ffffff; - } -} -.tab-content > .tab-pane { - display: none; -} -.tab-content > .active { - display: block; -} -.nav-tabs .dropdown-menu { - margin-top: -1px; - border-top-right-radius: 0; - border-top-left-radius: 0; -} -.navbar { - position: relative; - min-height: 64px; - margin-bottom: 23px; - border: 1px solid transparent; -} -@media (min-width: 768px) { - .navbar { - border-radius: 3px; - } -} -@media (min-width: 768px) { - .navbar-header { - float: left; - } -} -.navbar-collapse { - overflow-x: visible; - padding-right: 15px; - padding-left: 15px; - border-top: 1px solid transparent; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); - -webkit-overflow-scrolling: touch; -} -.navbar-collapse.in { - overflow-y: auto; -} -@media (min-width: 768px) { - .navbar-collapse { - width: auto; - border-top: 0; - -webkit-box-shadow: none; - box-shadow: none; - } - .navbar-collapse.collapse { - display: block !important; - height: auto !important; - padding-bottom: 0; - overflow: visible !important; - } - .navbar-collapse.in { - overflow-y: visible; - } - .navbar-fixed-top .navbar-collapse, - .navbar-static-top .navbar-collapse, - .navbar-fixed-bottom .navbar-collapse { - padding-left: 0; - padding-right: 0; - } -} -.navbar-fixed-top .navbar-collapse, -.navbar-fixed-bottom .navbar-collapse { - max-height: 340px; -} -@media (max-device-width: 480px) and (orientation: landscape) { - .navbar-fixed-top .navbar-collapse, - .navbar-fixed-bottom .navbar-collapse { - max-height: 200px; - } -} -.container > .navbar-header, -.container-fluid > .navbar-header, -.container > .navbar-collapse, -.container-fluid > .navbar-collapse { - margin-right: -15px; - margin-left: -15px; -} -@media (min-width: 768px) { - .container > .navbar-header, - .container-fluid > .navbar-header, - .container > .navbar-collapse, - .container-fluid > .navbar-collapse { - margin-right: 0; - margin-left: 0; - } -} -.navbar-static-top { - z-index: 1000; - border-width: 0 0 1px; -} -@media (min-width: 768px) { - .navbar-static-top { - border-radius: 0; - } -} -.navbar-fixed-top, -.navbar-fixed-bottom { - position: fixed; - right: 0; - left: 0; - z-index: 1030; -} -@media (min-width: 768px) { - .navbar-fixed-top, - .navbar-fixed-bottom { - border-radius: 0; - } -} -.navbar-fixed-top { - top: 0; - border-width: 0 0 1px; -} -.navbar-fixed-bottom { - bottom: 0; - margin-bottom: 0; - border-width: 1px 0 0; -} -.navbar-brand { - float: left; - padding: 20.5px 15px; - font-size: 17px; - line-height: 23px; - height: 64px; -} -.navbar-brand:hover, -.navbar-brand:focus { - text-decoration: none; -} -.navbar-brand > img { - display: block; -} -@media (min-width: 768px) { - .navbar > .container .navbar-brand, - .navbar > .container-fluid .navbar-brand { - margin-left: -15px; - } -} -.navbar-toggle { - position: relative; - float: right; - margin-right: 15px; - padding: 9px 10px; - margin-top: 15px; - margin-bottom: 15px; - background-color: transparent; - background-image: none; - border: 1px solid transparent; - border-radius: 3px; -} -.navbar-toggle:focus { - outline: 0; -} -.navbar-toggle .icon-bar { - display: block; - width: 22px; - height: 2px; - border-radius: 1px; -} -.navbar-toggle .icon-bar + .icon-bar { - margin-top: 4px; -} -@media (min-width: 768px) { - .navbar-toggle { - display: none; - } -} -.navbar-nav { - margin: 10.25px -15px; -} -.navbar-nav > li > a { - padding-top: 10px; - padding-bottom: 10px; - line-height: 23px; -} -@media (max-width: 767px) { - .navbar-nav .open .dropdown-menu { - position: static; - float: none; - width: auto; - margin-top: 0; - background-color: transparent; - border: 0; - -webkit-box-shadow: none; - box-shadow: none; - } - .navbar-nav .open .dropdown-menu > li > a, - .navbar-nav .open .dropdown-menu .dropdown-header { - padding: 5px 15px 5px 25px; - } - .navbar-nav .open .dropdown-menu > li > a { - line-height: 23px; - } - .navbar-nav .open .dropdown-menu > li > a:hover, - .navbar-nav .open .dropdown-menu > li > a:focus { - background-image: none; - } -} -@media (min-width: 768px) { - .navbar-nav { - float: left; - margin: 0; - } - .navbar-nav > li { - float: left; - } - .navbar-nav > li > a { - padding-top: 20.5px; - padding-bottom: 20.5px; - } -} -.navbar-form { - margin-left: -15px; - margin-right: -15px; - padding: 10px 15px; - border-top: 1px solid transparent; - border-bottom: 1px solid transparent; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); - margin-top: 13.5px; - margin-bottom: 13.5px; -} -@media (min-width: 768px) { - .navbar-form .form-group { - display: inline-block; - margin-bottom: 0; - vertical-align: middle; - } - .navbar-form .form-control { - display: inline-block; - width: auto; - vertical-align: middle; - } - .navbar-form .form-control-static { - display: inline-block; - } - .navbar-form .input-group { - display: inline-table; - vertical-align: middle; - } - .navbar-form .input-group .input-group-addon, - .navbar-form .input-group .input-group-btn, - .navbar-form .input-group .form-control { - width: auto; - } - .navbar-form .input-group > .form-control { - width: 100%; - } - .navbar-form .control-label { - margin-bottom: 0; - vertical-align: middle; - } - .navbar-form .radio, - .navbar-form .checkbox { - display: inline-block; - margin-top: 0; - margin-bottom: 0; - vertical-align: middle; - } - .navbar-form .radio label, - .navbar-form .checkbox label { - padding-left: 0; - } - .navbar-form .radio input[type="radio"], - .navbar-form .checkbox input[type="checkbox"] { - position: relative; - margin-left: 0; - } - .navbar-form .has-feedback .form-control-feedback { - top: 0; - } -} -@media (max-width: 767px) { - .navbar-form .form-group { - margin-bottom: 5px; - } - .navbar-form .form-group:last-child { - margin-bottom: 0; - } -} -@media (min-width: 768px) { - .navbar-form { - width: auto; - border: 0; - margin-left: 0; - margin-right: 0; - padding-top: 0; - padding-bottom: 0; - -webkit-box-shadow: none; - box-shadow: none; - } -} -.navbar-nav > li > .dropdown-menu { - margin-top: 0; - border-top-right-radius: 0; - border-top-left-radius: 0; -} -.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { - margin-bottom: 0; - border-top-right-radius: 3px; - border-top-left-radius: 3px; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; -} -.navbar-btn { - margin-top: 13.5px; - margin-bottom: 13.5px; -} -.navbar-btn.btn-sm { - margin-top: 17px; - margin-bottom: 17px; -} -.navbar-btn.btn-xs { - margin-top: 21px; - margin-bottom: 21px; -} -.navbar-text { - margin-top: 20.5px; - margin-bottom: 20.5px; -} -@media (min-width: 768px) { - .navbar-text { - float: left; - margin-left: 15px; - margin-right: 15px; - } -} -@media (min-width: 768px) { - .navbar-left { - float: left !important; - } - .navbar-right { - float: right !important; - margin-right: -15px; - } - .navbar-right ~ .navbar-right { - margin-right: 0; - } -} -.navbar-default { - background-color: #ffffff; - border-color: transparent; -} -.navbar-default .navbar-brand { - color: #666666; -} -.navbar-default .navbar-brand:hover, -.navbar-default .navbar-brand:focus { - color: #212121; - background-color: transparent; -} -.navbar-default .navbar-text { - color: #bbbbbb; -} -.navbar-default .navbar-nav > li > a { - color: #666666; -} -.navbar-default .navbar-nav > li > a:hover, -.navbar-default .navbar-nav > li > a:focus { - color: #212121; - background-color: transparent; -} -.navbar-default .navbar-nav > .active > a, -.navbar-default .navbar-nav > .active > a:hover, -.navbar-default .navbar-nav > .active > a:focus { - color: #212121; - background-color: #eeeeee; -} -.navbar-default .navbar-nav > .disabled > a, -.navbar-default .navbar-nav > .disabled > a:hover, -.navbar-default .navbar-nav > .disabled > a:focus { - color: #cccccc; - background-color: transparent; -} -.navbar-default .navbar-toggle { - border-color: transparent; -} -.navbar-default .navbar-toggle:hover, -.navbar-default .navbar-toggle:focus { - background-color: transparent; -} -.navbar-default .navbar-toggle .icon-bar { - background-color: rgba(0, 0, 0, 0.5); -} -.navbar-default .navbar-collapse, -.navbar-default .navbar-form { - border-color: transparent; -} -.navbar-default .navbar-nav > .open > a, -.navbar-default .navbar-nav > .open > a:hover, -.navbar-default .navbar-nav > .open > a:focus { - background-color: #eeeeee; - color: #212121; -} -@media (max-width: 767px) { - .navbar-default .navbar-nav .open .dropdown-menu > li > a { - color: #666666; - } - .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, - .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { - color: #212121; - background-color: transparent; - } - .navbar-default .navbar-nav .open .dropdown-menu > .active > a, - .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, - .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { - color: #212121; - background-color: #eeeeee; - } - .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, - .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, - .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { - color: #cccccc; - background-color: transparent; - } -} -.navbar-default .navbar-link { - color: #666666; -} -.navbar-default .navbar-link:hover { - color: #212121; -} -.navbar-default .btn-link { - color: #666666; -} -.navbar-default .btn-link:hover, -.navbar-default .btn-link:focus { - color: #212121; -} -.navbar-default .btn-link[disabled]:hover, -fieldset[disabled] .navbar-default .btn-link:hover, -.navbar-default .btn-link[disabled]:focus, -fieldset[disabled] .navbar-default .btn-link:focus { - color: #cccccc; -} -.navbar-inverse { - background-color: #2196f3; - border-color: transparent; -} -.navbar-inverse .navbar-brand { - color: #b2dbfb; -} -.navbar-inverse .navbar-brand:hover, -.navbar-inverse .navbar-brand:focus { - color: #ffffff; - background-color: transparent; -} -.navbar-inverse .navbar-text { - color: #bbbbbb; -} -.navbar-inverse .navbar-nav > li > a { - color: #b2dbfb; -} -.navbar-inverse .navbar-nav > li > a:hover, -.navbar-inverse .navbar-nav > li > a:focus { - color: #ffffff; - background-color: transparent; -} -.navbar-inverse .navbar-nav > .active > a, -.navbar-inverse .navbar-nav > .active > a:hover, -.navbar-inverse .navbar-nav > .active > a:focus { - color: #ffffff; - background-color: #0c7cd5; -} -.navbar-inverse .navbar-nav > .disabled > a, -.navbar-inverse .navbar-nav > .disabled > a:hover, -.navbar-inverse .navbar-nav > .disabled > a:focus { - color: #444444; - background-color: transparent; -} -.navbar-inverse .navbar-toggle { - border-color: transparent; -} -.navbar-inverse .navbar-toggle:hover, -.navbar-inverse .navbar-toggle:focus { - background-color: transparent; -} -.navbar-inverse .navbar-toggle .icon-bar { - background-color: rgba(0, 0, 0, 0.5); -} -.navbar-inverse .navbar-collapse, -.navbar-inverse .navbar-form { - border-color: #0c84e4; -} -.navbar-inverse .navbar-nav > .open > a, -.navbar-inverse .navbar-nav > .open > a:hover, -.navbar-inverse .navbar-nav > .open > a:focus { - background-color: #0c7cd5; - color: #ffffff; -} -@media (max-width: 767px) { - .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { - border-color: transparent; - } - .navbar-inverse .navbar-nav .open .dropdown-menu .divider { - background-color: transparent; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { - color: #b2dbfb; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, - .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { - color: #ffffff; - background-color: transparent; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, - .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, - .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { - color: #ffffff; - background-color: #0c7cd5; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, - .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, - .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { - color: #444444; - background-color: transparent; - } -} -.navbar-inverse .navbar-link { - color: #b2dbfb; -} -.navbar-inverse .navbar-link:hover { - color: #ffffff; -} -.navbar-inverse .btn-link { - color: #b2dbfb; -} -.navbar-inverse .btn-link:hover, -.navbar-inverse .btn-link:focus { - color: #ffffff; -} -.navbar-inverse .btn-link[disabled]:hover, -fieldset[disabled] .navbar-inverse .btn-link:hover, -.navbar-inverse .btn-link[disabled]:focus, -fieldset[disabled] .navbar-inverse .btn-link:focus { - color: #444444; -} -.breadcrumb { - padding: 8px 15px; - margin-bottom: 23px; - list-style: none; - background-color: #f5f5f5; - border-radius: 3px; -} -.breadcrumb > li { - display: inline-block; -} -.breadcrumb > li + li:before { - content: "/\00a0"; - padding: 0 5px; - color: #cccccc; -} -.breadcrumb > .active { - color: #bbbbbb; -} -.pagination { - display: inline-block; - padding-left: 0; - margin: 23px 0; - border-radius: 3px; -} -.pagination > li { - display: inline; -} -.pagination > li > a, -.pagination > li > span { - position: relative; - float: left; - padding: 6px 16px; - line-height: 1.846; - text-decoration: none; - color: #2196f3; - background-color: #ffffff; - border: 1px solid #dddddd; - margin-left: -1px; -} -.pagination > li:first-child > a, -.pagination > li:first-child > span { - margin-left: 0; - border-bottom-left-radius: 3px; - border-top-left-radius: 3px; -} -.pagination > li:last-child > a, -.pagination > li:last-child > span { - border-bottom-right-radius: 3px; - border-top-right-radius: 3px; -} -.pagination > li > a:hover, -.pagination > li > span:hover, -.pagination > li > a:focus, -.pagination > li > span:focus { - z-index: 2; - color: #0a6ebd; - background-color: #eeeeee; - border-color: #dddddd; -} -.pagination > .active > a, -.pagination > .active > span, -.pagination > .active > a:hover, -.pagination > .active > span:hover, -.pagination > .active > a:focus, -.pagination > .active > span:focus { - z-index: 3; - color: #ffffff; - background-color: #2196f3; - border-color: #2196f3; - cursor: default; -} -.pagination > .disabled > span, -.pagination > .disabled > span:hover, -.pagination > .disabled > span:focus, -.pagination > .disabled > a, -.pagination > .disabled > a:hover, -.pagination > .disabled > a:focus { - color: #bbbbbb; - background-color: #ffffff; - border-color: #dddddd; - cursor: not-allowed; -} -.pagination-lg > li > a, -.pagination-lg > li > span { - padding: 10px 16px; - font-size: 17px; - line-height: 1.3333333; -} -.pagination-lg > li:first-child > a, -.pagination-lg > li:first-child > span { - border-bottom-left-radius: 3px; - border-top-left-radius: 3px; -} -.pagination-lg > li:last-child > a, -.pagination-lg > li:last-child > span { - border-bottom-right-radius: 3px; - border-top-right-radius: 3px; -} -.pagination-sm > li > a, -.pagination-sm > li > span { - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; -} -.pagination-sm > li:first-child > a, -.pagination-sm > li:first-child > span { - border-bottom-left-radius: 3px; - border-top-left-radius: 3px; -} -.pagination-sm > li:last-child > a, -.pagination-sm > li:last-child > span { - border-bottom-right-radius: 3px; - border-top-right-radius: 3px; -} -.pager { - padding-left: 0; - margin: 23px 0; - list-style: none; - text-align: center; -} -.pager li { - display: inline; -} -.pager li > a, -.pager li > span { - display: inline-block; - padding: 5px 14px; - background-color: #ffffff; - border: 1px solid #dddddd; - border-radius: 15px; -} -.pager li > a:hover, -.pager li > a:focus { - text-decoration: none; - background-color: #eeeeee; -} -.pager .next > a, -.pager .next > span { - float: right; -} -.pager .previous > a, -.pager .previous > span { - float: left; -} -.pager .disabled > a, -.pager .disabled > a:hover, -.pager .disabled > a:focus, -.pager .disabled > span { - color: #bbbbbb; - background-color: #ffffff; - cursor: not-allowed; -} -.label { - display: inline; - padding: .2em .6em .3em; - font-size: 75%; - font-weight: bold; - line-height: 1; - color: #ffffff; - text-align: center; - white-space: nowrap; - vertical-align: baseline; - border-radius: .25em; -} -a.label:hover, -a.label:focus { - color: #ffffff; - text-decoration: none; - cursor: pointer; -} -.label:empty { - display: none; -} -.btn .label { - position: relative; - top: -1px; -} -.label-default { - background-color: #bbbbbb; -} -.label-default[href]:hover, -.label-default[href]:focus { - background-color: #a2a2a2; -} -.label-primary { - background-color: #2196f3; -} -.label-primary[href]:hover, -.label-primary[href]:focus { - background-color: #0c7cd5; -} -.label-success { - background-color: #4caf50; -} -.label-success[href]:hover, -.label-success[href]:focus { - background-color: #3d8b40; -} -.label-info { - background-color: #9c27b0; -} -.label-info[href]:hover, -.label-info[href]:focus { - background-color: #771e86; -} -.label-warning { - background-color: #ff9800; -} -.label-warning[href]:hover, -.label-warning[href]:focus { - background-color: #cc7a00; -} -.label-danger { - background-color: #e51c23; -} -.label-danger[href]:hover, -.label-danger[href]:focus { - background-color: #b9151b; -} -.badge { - display: inline-block; - min-width: 10px; - padding: 3px 7px; - font-size: 12px; - font-weight: normal; - color: #ffffff; - line-height: 1; - vertical-align: middle; - white-space: nowrap; - text-align: center; - background-color: #bbbbbb; - border-radius: 10px; -} -.badge:empty { - display: none; -} -.btn .badge { - position: relative; - top: -1px; -} -.btn-xs .badge, -.btn-group-xs > .btn .badge { - top: 0; - padding: 1px 5px; -} -a.badge:hover, -a.badge:focus { - color: #ffffff; - text-decoration: none; - cursor: pointer; -} -.list-group-item.active > .badge, -.nav-pills > .active > a > .badge { - color: #2196f3; - background-color: #ffffff; -} -.list-group-item > .badge { - float: right; -} -.list-group-item > .badge + .badge { - margin-right: 5px; -} -.nav-pills > li > a > .badge { - margin-left: 3px; -} -.jumbotron { - padding-top: 30px; - padding-bottom: 30px; - margin-bottom: 30px; - color: inherit; - background-color: #f9f9f9; -} -.jumbotron h1, -.jumbotron .h1 { - color: #444444; -} -.jumbotron p { - margin-bottom: 15px; - font-size: 20px; - font-weight: 200; -} -.jumbotron > hr { - border-top-color: #e0e0e0; -} -.container .jumbotron, -.container-fluid .jumbotron { - border-radius: 3px; - padding-left: 15px; - padding-right: 15px; -} -.jumbotron .container { - max-width: 100%; -} -@media screen and (min-width: 768px) { - .jumbotron { - padding-top: 48px; - padding-bottom: 48px; - } - .container .jumbotron, - .container-fluid .jumbotron { - padding-left: 60px; - padding-right: 60px; - } - .jumbotron h1, - .jumbotron .h1 { - font-size: 59px; - } -} -.thumbnail { - display: block; - padding: 4px; - margin-bottom: 23px; - line-height: 1.846; - background-color: #ffffff; - border: 1px solid #dddddd; - border-radius: 3px; - -webkit-transition: border 0.2s ease-in-out; - -o-transition: border 0.2s ease-in-out; - transition: border 0.2s ease-in-out; -} -.thumbnail > img, -.thumbnail a > img { - margin-left: auto; - margin-right: auto; -} -a.thumbnail:hover, -a.thumbnail:focus, -a.thumbnail.active { - border-color: #2196f3; -} -.thumbnail .caption { - padding: 9px; - color: #666666; -} -.alert { - padding: 15px; - margin-bottom: 23px; - border: 1px solid transparent; - border-radius: 3px; -} -.alert h4 { - margin-top: 0; - color: inherit; -} -.alert .alert-link { - font-weight: bold; -} -.alert > p, -.alert > ul { - margin-bottom: 0; -} -.alert > p + p { - margin-top: 5px; -} -.alert-dismissable, -.alert-dismissible { - padding-right: 35px; -} -.alert-dismissable .close, -.alert-dismissible .close { - position: relative; - top: -2px; - right: -21px; - color: inherit; -} -.alert-success { - background-color: #dff0d8; - border-color: #d6e9c6; - color: #4caf50; -} -.alert-success hr { - border-top-color: #c9e2b3; -} -.alert-success .alert-link { - color: #3d8b40; -} -.alert-info { - background-color: #e1bee7; - border-color: #cba4dd; - color: #9c27b0; -} -.alert-info hr { - border-top-color: #c191d6; -} -.alert-info .alert-link { - color: #771e86; -} -.alert-warning { - background-color: #ffe0b2; - border-color: #ffc599; - color: #ff9800; -} -.alert-warning hr { - border-top-color: #ffb67f; -} -.alert-warning .alert-link { - color: #cc7a00; -} -.alert-danger { - background-color: #f9bdbb; - border-color: #f7a4af; - color: #e51c23; -} -.alert-danger hr { - border-top-color: #f58c9a; -} -.alert-danger .alert-link { - color: #b9151b; -} -@-webkit-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} -@-o-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} -@keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} -.progress { - overflow: hidden; - height: 23px; - margin-bottom: 23px; - background-color: #f5f5f5; - border-radius: 3px; - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); -} -.progress-bar { - float: left; - width: 0%; - height: 100%; - font-size: 12px; - line-height: 23px; - color: #ffffff; - text-align: center; - background-color: #2196f3; - -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); - -webkit-transition: width 0.6s ease; - -o-transition: width 0.6s ease; - transition: width 0.6s ease; -} -.progress-striped .progress-bar, -.progress-bar-striped { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - -webkit-background-size: 40px 40px; - background-size: 40px 40px; -} -.progress.active .progress-bar, -.progress-bar.active { - -webkit-animation: progress-bar-stripes 2s linear infinite; - -o-animation: progress-bar-stripes 2s linear infinite; - animation: progress-bar-stripes 2s linear infinite; -} -.progress-bar-success { - background-color: #4caf50; -} -.progress-striped .progress-bar-success { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} -.progress-bar-info { - background-color: #9c27b0; -} -.progress-striped .progress-bar-info { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} -.progress-bar-warning { - background-color: #ff9800; -} -.progress-striped .progress-bar-warning { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} -.progress-bar-danger { - background-color: #e51c23; -} -.progress-striped .progress-bar-danger { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); -} -.media { - margin-top: 15px; -} -.media:first-child { - margin-top: 0; -} -.media, -.media-body { - zoom: 1; - overflow: hidden; -} -.media-body { - width: 10000px; -} -.media-object { - display: block; -} -.media-object.img-thumbnail { - max-width: none; -} -.media-right, -.media > .pull-right { - padding-left: 10px; -} -.media-left, -.media > .pull-left { - padding-right: 10px; -} -.media-left, -.media-right, -.media-body { - display: table-cell; - vertical-align: top; -} -.media-middle { - vertical-align: middle; -} -.media-bottom { - vertical-align: bottom; -} -.media-heading { - margin-top: 0; - margin-bottom: 5px; -} -.media-list { - padding-left: 0; - list-style: none; -} -.list-group { - margin-bottom: 20px; - padding-left: 0; -} -.list-group-item { - position: relative; - display: block; - padding: 10px 15px; - margin-bottom: -1px; - background-color: #ffffff; - border: 1px solid #dddddd; -} -.list-group-item:first-child { - border-top-right-radius: 3px; - border-top-left-radius: 3px; -} -.list-group-item:last-child { - margin-bottom: 0; - border-bottom-right-radius: 3px; - border-bottom-left-radius: 3px; -} -a.list-group-item, -button.list-group-item { - color: #555555; -} -a.list-group-item .list-group-item-heading, -button.list-group-item .list-group-item-heading { - color: #333333; -} -a.list-group-item:hover, -button.list-group-item:hover, -a.list-group-item:focus, -button.list-group-item:focus { - text-decoration: none; - color: #555555; - background-color: #f5f5f5; -} -button.list-group-item { - width: 100%; - text-align: left; -} -.list-group-item.disabled, -.list-group-item.disabled:hover, -.list-group-item.disabled:focus { - background-color: #eeeeee; - color: #bbbbbb; - cursor: not-allowed; -} -.list-group-item.disabled .list-group-item-heading, -.list-group-item.disabled:hover .list-group-item-heading, -.list-group-item.disabled:focus .list-group-item-heading { - color: inherit; -} -.list-group-item.disabled .list-group-item-text, -.list-group-item.disabled:hover .list-group-item-text, -.list-group-item.disabled:focus .list-group-item-text { - color: #bbbbbb; -} -.list-group-item.active, -.list-group-item.active:hover, -.list-group-item.active:focus { - z-index: 2; - color: #ffffff; - background-color: #2196f3; - border-color: #2196f3; -} -.list-group-item.active .list-group-item-heading, -.list-group-item.active:hover .list-group-item-heading, -.list-group-item.active:focus .list-group-item-heading, -.list-group-item.active .list-group-item-heading > small, -.list-group-item.active:hover .list-group-item-heading > small, -.list-group-item.active:focus .list-group-item-heading > small, -.list-group-item.active .list-group-item-heading > .small, -.list-group-item.active:hover .list-group-item-heading > .small, -.list-group-item.active:focus .list-group-item-heading > .small { - color: inherit; -} -.list-group-item.active .list-group-item-text, -.list-group-item.active:hover .list-group-item-text, -.list-group-item.active:focus .list-group-item-text { - color: #e3f2fd; -} -.list-group-item-success { - color: #4caf50; - background-color: #dff0d8; -} -a.list-group-item-success, -button.list-group-item-success { - color: #4caf50; -} -a.list-group-item-success .list-group-item-heading, -button.list-group-item-success .list-group-item-heading { - color: inherit; -} -a.list-group-item-success:hover, -button.list-group-item-success:hover, -a.list-group-item-success:focus, -button.list-group-item-success:focus { - color: #4caf50; - background-color: #d0e9c6; -} -a.list-group-item-success.active, -button.list-group-item-success.active, -a.list-group-item-success.active:hover, -button.list-group-item-success.active:hover, -a.list-group-item-success.active:focus, -button.list-group-item-success.active:focus { - color: #fff; - background-color: #4caf50; - border-color: #4caf50; -} -.list-group-item-info { - color: #9c27b0; - background-color: #e1bee7; -} -a.list-group-item-info, -button.list-group-item-info { - color: #9c27b0; -} -a.list-group-item-info .list-group-item-heading, -button.list-group-item-info .list-group-item-heading { - color: inherit; -} -a.list-group-item-info:hover, -button.list-group-item-info:hover, -a.list-group-item-info:focus, -button.list-group-item-info:focus { - color: #9c27b0; - background-color: #d8abe0; -} -a.list-group-item-info.active, -button.list-group-item-info.active, -a.list-group-item-info.active:hover, -button.list-group-item-info.active:hover, -a.list-group-item-info.active:focus, -button.list-group-item-info.active:focus { - color: #fff; - background-color: #9c27b0; - border-color: #9c27b0; -} -.list-group-item-warning { - color: #ff9800; - background-color: #ffe0b2; -} -a.list-group-item-warning, -button.list-group-item-warning { - color: #ff9800; -} -a.list-group-item-warning .list-group-item-heading, -button.list-group-item-warning .list-group-item-heading { - color: inherit; -} -a.list-group-item-warning:hover, -button.list-group-item-warning:hover, -a.list-group-item-warning:focus, -button.list-group-item-warning:focus { - color: #ff9800; - background-color: #ffd699; -} -a.list-group-item-warning.active, -button.list-group-item-warning.active, -a.list-group-item-warning.active:hover, -button.list-group-item-warning.active:hover, -a.list-group-item-warning.active:focus, -button.list-group-item-warning.active:focus { - color: #fff; - background-color: #ff9800; - border-color: #ff9800; -} -.list-group-item-danger { - color: #e51c23; - background-color: #f9bdbb; -} -a.list-group-item-danger, -button.list-group-item-danger { - color: #e51c23; -} -a.list-group-item-danger .list-group-item-heading, -button.list-group-item-danger .list-group-item-heading { - color: inherit; -} -a.list-group-item-danger:hover, -button.list-group-item-danger:hover, -a.list-group-item-danger:focus, -button.list-group-item-danger:focus { - color: #e51c23; - background-color: #f7a6a4; -} -a.list-group-item-danger.active, -button.list-group-item-danger.active, -a.list-group-item-danger.active:hover, -button.list-group-item-danger.active:hover, -a.list-group-item-danger.active:focus, -button.list-group-item-danger.active:focus { - color: #fff; - background-color: #e51c23; - border-color: #e51c23; -} -.list-group-item-heading { - margin-top: 0; - margin-bottom: 5px; -} -.list-group-item-text { - margin-bottom: 0; - line-height: 1.3; -} -.panel { - margin-bottom: 23px; - background-color: #ffffff; - border: 1px solid transparent; - border-radius: 3px; - -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); -} -.panel-body { - padding: 15px; -} -.panel-heading { - padding: 10px 15px; - border-bottom: 1px solid transparent; - border-top-right-radius: 2px; - border-top-left-radius: 2px; -} -.panel-heading > .dropdown .dropdown-toggle { - color: inherit; -} -.panel-title { - margin-top: 0; - margin-bottom: 0; - font-size: 15px; - color: inherit; -} -.panel-title > a, -.panel-title > small, -.panel-title > .small, -.panel-title > small > a, -.panel-title > .small > a { - color: inherit; -} -.panel-footer { - padding: 10px 15px; - background-color: #f5f5f5; - border-top: 1px solid #dddddd; - border-bottom-right-radius: 2px; - border-bottom-left-radius: 2px; -} -.panel > .list-group, -.panel > .panel-collapse > .list-group { - margin-bottom: 0; -} -.panel > .list-group .list-group-item, -.panel > .panel-collapse > .list-group .list-group-item { - border-width: 1px 0; - border-radius: 0; -} -.panel > .list-group:first-child .list-group-item:first-child, -.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child { - border-top: 0; - border-top-right-radius: 2px; - border-top-left-radius: 2px; -} -.panel > .list-group:last-child .list-group-item:last-child, -.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child { - border-bottom: 0; - border-bottom-right-radius: 2px; - border-bottom-left-radius: 2px; -} -.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child { - border-top-right-radius: 0; - border-top-left-radius: 0; -} -.panel-heading + .list-group .list-group-item:first-child { - border-top-width: 0; -} -.list-group + .panel-footer { - border-top-width: 0; -} -.panel > .table, -.panel > .table-responsive > .table, -.panel > .panel-collapse > .table { - margin-bottom: 0; -} -.panel > .table caption, -.panel > .table-responsive > .table caption, -.panel > .panel-collapse > .table caption { - padding-left: 15px; - padding-right: 15px; -} -.panel > .table:first-child, -.panel > .table-responsive:first-child > .table:first-child { - border-top-right-radius: 2px; - border-top-left-radius: 2px; -} -.panel > .table:first-child > thead:first-child > tr:first-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child, -.panel > .table:first-child > tbody:first-child > tr:first-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child { - border-top-left-radius: 2px; - border-top-right-radius: 2px; -} -.panel > .table:first-child > thead:first-child > tr:first-child td:first-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, -.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, -.panel > .table:first-child > thead:first-child > tr:first-child th:first-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, -.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { - border-top-left-radius: 2px; -} -.panel > .table:first-child > thead:first-child > tr:first-child td:last-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, -.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, -.panel > .table:first-child > thead:first-child > tr:first-child th:last-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, -.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { - border-top-right-radius: 2px; -} -.panel > .table:last-child, -.panel > .table-responsive:last-child > .table:last-child { - border-bottom-right-radius: 2px; - border-bottom-left-radius: 2px; -} -.panel > .table:last-child > tbody:last-child > tr:last-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child { - border-bottom-left-radius: 2px; - border-bottom-right-radius: 2px; -} -.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, -.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { - border-bottom-left-radius: 2px; -} -.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, -.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child { - border-bottom-right-radius: 2px; -} -.panel > .panel-body + .table, -.panel > .panel-body + .table-responsive, -.panel > .table + .panel-body, -.panel > .table-responsive + .panel-body { - border-top: 1px solid #dddddd; -} -.panel > .table > tbody:first-child > tr:first-child th, -.panel > .table > tbody:first-child > tr:first-child td { - border-top: 0; -} -.panel > .table-bordered, -.panel > .table-responsive > .table-bordered { - border: 0; -} -.panel > .table-bordered > thead > tr > th:first-child, -.panel > .table-responsive > .table-bordered > thead > tr > th:first-child, -.panel > .table-bordered > tbody > tr > th:first-child, -.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, -.panel > .table-bordered > tfoot > tr > th:first-child, -.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, -.panel > .table-bordered > thead > tr > td:first-child, -.panel > .table-responsive > .table-bordered > thead > tr > td:first-child, -.panel > .table-bordered > tbody > tr > td:first-child, -.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, -.panel > .table-bordered > tfoot > tr > td:first-child, -.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { - border-left: 0; -} -.panel > .table-bordered > thead > tr > th:last-child, -.panel > .table-responsive > .table-bordered > thead > tr > th:last-child, -.panel > .table-bordered > tbody > tr > th:last-child, -.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, -.panel > .table-bordered > tfoot > tr > th:last-child, -.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, -.panel > .table-bordered > thead > tr > td:last-child, -.panel > .table-responsive > .table-bordered > thead > tr > td:last-child, -.panel > .table-bordered > tbody > tr > td:last-child, -.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, -.panel > .table-bordered > tfoot > tr > td:last-child, -.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { - border-right: 0; -} -.panel > .table-bordered > thead > tr:first-child > td, -.panel > .table-responsive > .table-bordered > thead > tr:first-child > td, -.panel > .table-bordered > tbody > tr:first-child > td, -.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, -.panel > .table-bordered > thead > tr:first-child > th, -.panel > .table-responsive > .table-bordered > thead > tr:first-child > th, -.panel > .table-bordered > tbody > tr:first-child > th, -.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { - border-bottom: 0; -} -.panel > .table-bordered > tbody > tr:last-child > td, -.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, -.panel > .table-bordered > tfoot > tr:last-child > td, -.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, -.panel > .table-bordered > tbody > tr:last-child > th, -.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, -.panel > .table-bordered > tfoot > tr:last-child > th, -.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { - border-bottom: 0; -} -.panel > .table-responsive { - border: 0; - margin-bottom: 0; -} -.panel-group { - margin-bottom: 23px; -} -.panel-group .panel { - margin-bottom: 0; - border-radius: 3px; -} -.panel-group .panel + .panel { - margin-top: 5px; -} -.panel-group .panel-heading { - border-bottom: 0; -} -.panel-group .panel-heading + .panel-collapse > .panel-body, -.panel-group .panel-heading + .panel-collapse > .list-group { - border-top: 1px solid #dddddd; -} -.panel-group .panel-footer { - border-top: 0; -} -.panel-group .panel-footer + .panel-collapse .panel-body { - border-bottom: 1px solid #dddddd; -} -.panel-default { - border-color: #dddddd; -} -.panel-default > .panel-heading { - color: #212121; - background-color: #f5f5f5; - border-color: #dddddd; -} -.panel-default > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #dddddd; -} -.panel-default > .panel-heading .badge { - color: #f5f5f5; - background-color: #212121; -} -.panel-default > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #dddddd; -} -.panel-primary { - border-color: #2196f3; -} -.panel-primary > .panel-heading { - color: #ffffff; - background-color: #2196f3; - border-color: #2196f3; -} -.panel-primary > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #2196f3; -} -.panel-primary > .panel-heading .badge { - color: #2196f3; - background-color: #ffffff; -} -.panel-primary > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #2196f3; -} -.panel-success { - border-color: #d6e9c6; -} -.panel-success > .panel-heading { - color: #ffffff; - background-color: #4caf50; - border-color: #d6e9c6; -} -.panel-success > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #d6e9c6; -} -.panel-success > .panel-heading .badge { - color: #4caf50; - background-color: #ffffff; -} -.panel-success > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #d6e9c6; -} -.panel-info { - border-color: #cba4dd; -} -.panel-info > .panel-heading { - color: #ffffff; - background-color: #9c27b0; - border-color: #cba4dd; -} -.panel-info > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #cba4dd; -} -.panel-info > .panel-heading .badge { - color: #9c27b0; - background-color: #ffffff; -} -.panel-info > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #cba4dd; -} -.panel-warning { - border-color: #ffc599; -} -.panel-warning > .panel-heading { - color: #ffffff; - background-color: #ff9800; - border-color: #ffc599; -} -.panel-warning > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #ffc599; -} -.panel-warning > .panel-heading .badge { - color: #ff9800; - background-color: #ffffff; -} -.panel-warning > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #ffc599; -} -.panel-danger { - border-color: #f7a4af; -} -.panel-danger > .panel-heading { - color: #ffffff; - background-color: #e51c23; - border-color: #f7a4af; -} -.panel-danger > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #f7a4af; -} -.panel-danger > .panel-heading .badge { - color: #e51c23; - background-color: #ffffff; -} -.panel-danger > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #f7a4af; -} -.embed-responsive { - position: relative; - display: block; - height: 0; - padding: 0; - overflow: hidden; -} -.embed-responsive .embed-responsive-item, -.embed-responsive iframe, -.embed-responsive embed, -.embed-responsive object, -.embed-responsive video { - position: absolute; - top: 0; - left: 0; - bottom: 0; - height: 100%; - width: 100%; - border: 0; -} -.embed-responsive-16by9 { - padding-bottom: 56.25%; -} -.embed-responsive-4by3 { - padding-bottom: 75%; -} -.well { - min-height: 20px; - padding: 19px; - margin-bottom: 20px; - background-color: #f9f9f9; - border: 1px solid transparent; - border-radius: 3px; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); -} -.well blockquote { - border-color: #ddd; - border-color: rgba(0, 0, 0, 0.15); -} -.well-lg { - padding: 24px; - border-radius: 3px; -} -.well-sm { - padding: 9px; - border-radius: 3px; -} -.close { - float: right; - font-size: 19.5px; - font-weight: normal; - line-height: 1; - color: #000000; - text-shadow: none; - opacity: 0.2; - filter: alpha(opacity=20); -} -.close:hover, -.close:focus { - color: #000000; - text-decoration: none; - cursor: pointer; - opacity: 0.5; - filter: alpha(opacity=50); -} -button.close { - padding: 0; - cursor: pointer; - background: transparent; - border: 0; - -webkit-appearance: none; -} -.modal-open { - overflow: hidden; -} -.modal { - display: none; - overflow: hidden; - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1050; - -webkit-overflow-scrolling: touch; - outline: 0; -} -.modal.fade .modal-dialog { - -webkit-transform: translate(0, -25%); - -ms-transform: translate(0, -25%); - -o-transform: translate(0, -25%); - transform: translate(0, -25%); - -webkit-transition: -webkit-transform 0.3s ease-out; - -o-transition: -o-transform 0.3s ease-out; - transition: transform 0.3s ease-out; -} -.modal.in .modal-dialog { - -webkit-transform: translate(0, 0); - -ms-transform: translate(0, 0); - -o-transform: translate(0, 0); - transform: translate(0, 0); -} -.modal-open .modal { - overflow-x: hidden; - overflow-y: auto; -} -.modal-dialog { - position: relative; - width: auto; - margin: 10px; -} -.modal-content { - position: relative; - background-color: #ffffff; - border: 1px solid #999999; - border: 1px solid transparent; - border-radius: 3px; - -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); - box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); - -webkit-background-clip: padding-box; - background-clip: padding-box; - outline: 0; -} -.modal-backdrop { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1040; - background-color: #000000; -} -.modal-backdrop.fade { - opacity: 0; - filter: alpha(opacity=0); -} -.modal-backdrop.in { - opacity: 0.5; - filter: alpha(opacity=50); -} -.modal-header { - padding: 15px; - border-bottom: 1px solid transparent; -} -.modal-header .close { - margin-top: -2px; -} -.modal-title { - margin: 0; - line-height: 1.846; -} -.modal-body { - position: relative; - padding: 15px; -} -.modal-footer { - padding: 15px; - text-align: right; - border-top: 1px solid transparent; -} -.modal-footer .btn + .btn { - margin-left: 5px; - margin-bottom: 0; -} -.modal-footer .btn-group .btn + .btn { - margin-left: -1px; -} -.modal-footer .btn-block + .btn-block { - margin-left: 0; -} -.modal-scrollbar-measure { - position: absolute; - top: -9999px; - width: 50px; - height: 50px; - overflow: scroll; -} -@media (min-width: 768px) { - .modal-dialog { - width: 600px; - margin: 30px auto; - } - .modal-content { - -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); - } - .modal-sm { - width: 300px; - } -} -@media (min-width: 992px) { - .modal-lg { - width: 900px; - } -} -.tooltip { - position: absolute; - z-index: 1070; - display: block; - font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; - font-style: normal; - font-weight: normal; - letter-spacing: normal; - line-break: auto; - line-height: 1.846; - text-align: left; - text-align: start; - text-decoration: none; - text-shadow: none; - text-transform: none; - white-space: normal; - word-break: normal; - word-spacing: normal; - word-wrap: normal; - font-size: 12px; - opacity: 0; - filter: alpha(opacity=0); -} -.tooltip.in { - opacity: 0.9; - filter: alpha(opacity=90); -} -.tooltip.top { - margin-top: -3px; - padding: 5px 0; -} -.tooltip.right { - margin-left: 3px; - padding: 0 5px; -} -.tooltip.bottom { - margin-top: 3px; - padding: 5px 0; -} -.tooltip.left { - margin-left: -3px; - padding: 0 5px; -} -.tooltip-inner { - max-width: 200px; - padding: 3px 8px; - color: #ffffff; - text-align: center; - background-color: #727272; - border-radius: 3px; -} -.tooltip-arrow { - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; -} -.tooltip.top .tooltip-arrow { - bottom: 0; - left: 50%; - margin-left: -5px; - border-width: 5px 5px 0; - border-top-color: #727272; -} -.tooltip.top-left .tooltip-arrow { - bottom: 0; - right: 5px; - margin-bottom: -5px; - border-width: 5px 5px 0; - border-top-color: #727272; -} -.tooltip.top-right .tooltip-arrow { - bottom: 0; - left: 5px; - margin-bottom: -5px; - border-width: 5px 5px 0; - border-top-color: #727272; -} -.tooltip.right .tooltip-arrow { - top: 50%; - left: 0; - margin-top: -5px; - border-width: 5px 5px 5px 0; - border-right-color: #727272; -} -.tooltip.left .tooltip-arrow { - top: 50%; - right: 0; - margin-top: -5px; - border-width: 5px 0 5px 5px; - border-left-color: #727272; -} -.tooltip.bottom .tooltip-arrow { - top: 0; - left: 50%; - margin-left: -5px; - border-width: 0 5px 5px; - border-bottom-color: #727272; -} -.tooltip.bottom-left .tooltip-arrow { - top: 0; - right: 5px; - margin-top: -5px; - border-width: 0 5px 5px; - border-bottom-color: #727272; -} -.tooltip.bottom-right .tooltip-arrow { - top: 0; - left: 5px; - margin-top: -5px; - border-width: 0 5px 5px; - border-bottom-color: #727272; -} -.popover { - position: absolute; - top: 0; - left: 0; - z-index: 1060; - display: none; - max-width: 276px; - padding: 1px; - font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif; - font-style: normal; - font-weight: normal; - letter-spacing: normal; - line-break: auto; - line-height: 1.846; - text-align: left; - text-align: start; - text-decoration: none; - text-shadow: none; - text-transform: none; - white-space: normal; - word-break: normal; - word-spacing: normal; - word-wrap: normal; - font-size: 13px; - background-color: #ffffff; - -webkit-background-clip: padding-box; - background-clip: padding-box; - border: 1px solid transparent; - border-radius: 3px; - -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); - box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); -} -.popover.top { - margin-top: -10px; -} -.popover.right { - margin-left: 10px; -} -.popover.bottom { - margin-top: 10px; -} -.popover.left { - margin-left: -10px; -} -.popover-title { - margin: 0; - padding: 8px 14px; - font-size: 13px; - background-color: #f7f7f7; - border-bottom: 1px solid #ebebeb; - border-radius: 2px 2px 0 0; -} -.popover-content { - padding: 9px 14px; -} -.popover > .arrow, -.popover > .arrow:after { - position: absolute; - display: block; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; -} -.popover > .arrow { - border-width: 11px; -} -.popover > .arrow:after { - border-width: 10px; - content: ""; -} -.popover.top > .arrow { - left: 50%; - margin-left: -11px; - border-bottom-width: 0; - border-top-color: rgba(0, 0, 0, 0); - border-top-color: rgba(0, 0, 0, 0.075); - bottom: -11px; -} -.popover.top > .arrow:after { - content: " "; - bottom: 1px; - margin-left: -10px; - border-bottom-width: 0; - border-top-color: #ffffff; -} -.popover.right > .arrow { - top: 50%; - left: -11px; - margin-top: -11px; - border-left-width: 0; - border-right-color: rgba(0, 0, 0, 0); - border-right-color: rgba(0, 0, 0, 0.075); -} -.popover.right > .arrow:after { - content: " "; - left: 1px; - bottom: -10px; - border-left-width: 0; - border-right-color: #ffffff; -} -.popover.bottom > .arrow { - left: 50%; - margin-left: -11px; - border-top-width: 0; - border-bottom-color: rgba(0, 0, 0, 0); - border-bottom-color: rgba(0, 0, 0, 0.075); - top: -11px; -} -.popover.bottom > .arrow:after { - content: " "; - top: 1px; - margin-left: -10px; - border-top-width: 0; - border-bottom-color: #ffffff; -} -.popover.left > .arrow { - top: 50%; - right: -11px; - margin-top: -11px; - border-right-width: 0; - border-left-color: rgba(0, 0, 0, 0); - border-left-color: rgba(0, 0, 0, 0.075); -} -.popover.left > .arrow:after { - content: " "; - right: 1px; - border-right-width: 0; - border-left-color: #ffffff; - bottom: -10px; -} -.carousel { - position: relative; -} -.carousel-inner { - position: relative; - overflow: hidden; - width: 100%; -} -.carousel-inner > .item { - display: none; - position: relative; - -webkit-transition: 0.6s ease-in-out left; - -o-transition: 0.6s ease-in-out left; - transition: 0.6s ease-in-out left; -} -.carousel-inner > .item > img, -.carousel-inner > .item > a > img { - line-height: 1; -} -@media all and (transform-3d), (-webkit-transform-3d) { - .carousel-inner > .item { - -webkit-transition: -webkit-transform 0.6s ease-in-out; - -o-transition: -o-transform 0.6s ease-in-out; - transition: transform 0.6s ease-in-out; - -webkit-backface-visibility: hidden; - backface-visibility: hidden; - -webkit-perspective: 1000px; - perspective: 1000px; - } - .carousel-inner > .item.next, - .carousel-inner > .item.active.right { - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); - left: 0; - } - .carousel-inner > .item.prev, - .carousel-inner > .item.active.left { - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); - left: 0; - } - .carousel-inner > .item.next.left, - .carousel-inner > .item.prev.right, - .carousel-inner > .item.active { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - left: 0; - } -} -.carousel-inner > .active, -.carousel-inner > .next, -.carousel-inner > .prev { - display: block; -} -.carousel-inner > .active { - left: 0; -} -.carousel-inner > .next, -.carousel-inner > .prev { - position: absolute; - top: 0; - width: 100%; -} -.carousel-inner > .next { - left: 100%; -} -.carousel-inner > .prev { - left: -100%; -} -.carousel-inner > .next.left, -.carousel-inner > .prev.right { - left: 0; -} -.carousel-inner > .active.left { - left: -100%; -} -.carousel-inner > .active.right { - left: 100%; -} -.carousel-control { - position: absolute; - top: 0; - left: 0; - bottom: 0; - width: 15%; - opacity: 0.5; - filter: alpha(opacity=50); - font-size: 20px; - color: #ffffff; - text-align: center; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); - background-color: rgba(0, 0, 0, 0); -} -.carousel-control.left { - background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%); - background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%); - background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0.0001))); - background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); -} -.carousel-control.right { - left: auto; - right: 0; - background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); - background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); - background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, 0.0001)), to(rgba(0, 0, 0, 0.5))); - background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%); - background-repeat: repeat-x; - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); -} -.carousel-control:hover, -.carousel-control:focus { - outline: 0; - color: #ffffff; - text-decoration: none; - opacity: 0.9; - filter: alpha(opacity=90); -} -.carousel-control .icon-prev, -.carousel-control .icon-next, -.carousel-control .glyphicon-chevron-left, -.carousel-control .glyphicon-chevron-right { - position: absolute; - top: 50%; - margin-top: -10px; - z-index: 5; - display: inline-block; -} -.carousel-control .icon-prev, -.carousel-control .glyphicon-chevron-left { - left: 50%; - margin-left: -10px; -} -.carousel-control .icon-next, -.carousel-control .glyphicon-chevron-right { - right: 50%; - margin-right: -10px; -} -.carousel-control .icon-prev, -.carousel-control .icon-next { - width: 20px; - height: 20px; - line-height: 1; - font-family: serif; -} -.carousel-control .icon-prev:before { - content: '\2039'; -} -.carousel-control .icon-next:before { - content: '\203a'; -} -.carousel-indicators { - position: absolute; - bottom: 10px; - left: 50%; - z-index: 15; - width: 60%; - margin-left: -30%; - padding-left: 0; - list-style: none; - text-align: center; -} -.carousel-indicators li { - display: inline-block; - width: 10px; - height: 10px; - margin: 1px; - text-indent: -999px; - border: 1px solid #ffffff; - border-radius: 10px; - cursor: pointer; - background-color: #000 \9; - background-color: rgba(0, 0, 0, 0); -} -.carousel-indicators .active { - margin: 0; - width: 12px; - height: 12px; - background-color: #ffffff; -} -.carousel-caption { - position: absolute; - left: 15%; - right: 15%; - bottom: 20px; - z-index: 10; - padding-top: 20px; - padding-bottom: 20px; - color: #ffffff; - text-align: center; - text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); -} -.carousel-caption .btn { - text-shadow: none; -} -@media screen and (min-width: 768px) { - .carousel-control .glyphicon-chevron-left, - .carousel-control .glyphicon-chevron-right, - .carousel-control .icon-prev, - .carousel-control .icon-next { - width: 30px; - height: 30px; - margin-top: -10px; - font-size: 30px; - } - .carousel-control .glyphicon-chevron-left, - .carousel-control .icon-prev { - margin-left: -10px; - } - .carousel-control .glyphicon-chevron-right, - .carousel-control .icon-next { - margin-right: -10px; - } - .carousel-caption { - left: 20%; - right: 20%; - padding-bottom: 30px; - } - .carousel-indicators { - bottom: 20px; - } -} -.clearfix:before, -.clearfix:after, -.dl-horizontal dd:before, -.dl-horizontal dd:after, -.container:before, -.container:after, -.container-fluid:before, -.container-fluid:after, -.row:before, -.row:after, -.form-horizontal .form-group:before, -.form-horizontal .form-group:after, -.btn-toolbar:before, -.btn-toolbar:after, -.btn-group-vertical > .btn-group:before, -.btn-group-vertical > .btn-group:after, -.nav:before, -.nav:after, -.navbar:before, -.navbar:after, -.navbar-header:before, -.navbar-header:after, -.navbar-collapse:before, -.navbar-collapse:after, -.pager:before, -.pager:after, -.panel-body:before, -.panel-body:after, -.modal-header:before, -.modal-header:after, -.modal-footer:before, -.modal-footer:after { - content: " "; - display: table; -} -.clearfix:after, -.dl-horizontal dd:after, -.container:after, -.container-fluid:after, -.row:after, -.form-horizontal .form-group:after, -.btn-toolbar:after, -.btn-group-vertical > .btn-group:after, -.nav:after, -.navbar:after, -.navbar-header:after, -.navbar-collapse:after, -.pager:after, -.panel-body:after, -.modal-header:after, -.modal-footer:after { - clear: both; -} -.center-block { - display: block; - margin-left: auto; - margin-right: auto; -} -.pull-right { - float: right !important; -} -.pull-left { - float: left !important; -} -.hide { - display: none !important; -} -.show { - display: block !important; -} -.invisible { - visibility: hidden; -} -.text-hide { - font: 0/0 a; - color: transparent; - text-shadow: none; - background-color: transparent; - border: 0; -} -.hidden { - display: none !important; -} -.affix { - position: fixed; -} -@-ms-viewport { - width: device-width; -} -.visible-xs, -.visible-sm, -.visible-md, -.visible-lg { - display: none !important; -} -.visible-xs-block, -.visible-xs-inline, -.visible-xs-inline-block, -.visible-sm-block, -.visible-sm-inline, -.visible-sm-inline-block, -.visible-md-block, -.visible-md-inline, -.visible-md-inline-block, -.visible-lg-block, -.visible-lg-inline, -.visible-lg-inline-block { - display: none !important; -} -@media (max-width: 767px) { - .visible-xs { - display: block !important; - } - table.visible-xs { - display: table !important; - } - tr.visible-xs { - display: table-row !important; - } - th.visible-xs, - td.visible-xs { - display: table-cell !important; - } -} -@media (max-width: 767px) { - .visible-xs-block { - display: block !important; - } -} -@media (max-width: 767px) { - .visible-xs-inline { - display: inline !important; - } -} -@media (max-width: 767px) { - .visible-xs-inline-block { - display: inline-block !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .visible-sm { - display: block !important; - } - table.visible-sm { - display: table !important; - } - tr.visible-sm { - display: table-row !important; - } - th.visible-sm, - td.visible-sm { - display: table-cell !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .visible-sm-block { - display: block !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .visible-sm-inline { - display: inline !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .visible-sm-inline-block { - display: inline-block !important; - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .visible-md { - display: block !important; - } - table.visible-md { - display: table !important; - } - tr.visible-md { - display: table-row !important; - } - th.visible-md, - td.visible-md { - display: table-cell !important; - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .visible-md-block { - display: block !important; - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .visible-md-inline { - display: inline !important; - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .visible-md-inline-block { - display: inline-block !important; - } -} -@media (min-width: 1200px) { - .visible-lg { - display: block !important; - } - table.visible-lg { - display: table !important; - } - tr.visible-lg { - display: table-row !important; - } - th.visible-lg, - td.visible-lg { - display: table-cell !important; - } -} -@media (min-width: 1200px) { - .visible-lg-block { - display: block !important; - } -} -@media (min-width: 1200px) { - .visible-lg-inline { - display: inline !important; - } -} -@media (min-width: 1200px) { - .visible-lg-inline-block { - display: inline-block !important; - } -} -@media (max-width: 767px) { - .hidden-xs { - display: none !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .hidden-sm { - display: none !important; - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .hidden-md { - display: none !important; - } -} -@media (min-width: 1200px) { - .hidden-lg { - display: none !important; - } -} -.visible-print { - display: none !important; -} -@media print { - .visible-print { - display: block !important; - } - table.visible-print { - display: table !important; - } - tr.visible-print { - display: table-row !important; - } - th.visible-print, - td.visible-print { - display: table-cell !important; - } -} -.visible-print-block { - display: none !important; -} -@media print { - .visible-print-block { - display: block !important; - } -} -.visible-print-inline { - display: none !important; -} -@media print { - .visible-print-inline { - display: inline !important; - } -} -.visible-print-inline-block { - display: none !important; -} -@media print { - .visible-print-inline-block { - display: inline-block !important; - } -} -@media print { - .hidden-print { - display: none !important; - } -} -.navbar { - border: none; - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); -} -.navbar-brand { - font-size: 24px; -} -.navbar-inverse .navbar-form input[type=text], -.navbar-inverse .navbar-form input[type=password] { - color: #fff; - -webkit-box-shadow: inset 0 -1px 0 #b2dbfb; - box-shadow: inset 0 -1px 0 #b2dbfb; -} -.navbar-inverse .navbar-form input[type=text]::-moz-placeholder, -.navbar-inverse .navbar-form input[type=password]::-moz-placeholder { - color: #b2dbfb; - opacity: 1; -} -.navbar-inverse .navbar-form input[type=text]:-ms-input-placeholder, -.navbar-inverse .navbar-form input[type=password]:-ms-input-placeholder { - color: #b2dbfb; -} -.navbar-inverse .navbar-form input[type=text]::-webkit-input-placeholder, -.navbar-inverse .navbar-form input[type=password]::-webkit-input-placeholder { - color: #b2dbfb; -} -.navbar-inverse .navbar-form input[type=text]:focus, -.navbar-inverse .navbar-form input[type=password]:focus { - -webkit-box-shadow: inset 0 -2px 0 #ffffff; - box-shadow: inset 0 -2px 0 #ffffff; -} -.btn-default { - -webkit-background-size: 200% 200%; - background-size: 200% 200%; - background-position: 50%; -} -.btn-default:focus { - background-color: #ffffff; -} -.btn-default:hover, -.btn-default:active:hover { - background-color: #f0f0f0; -} -.btn-default:active { - background-color: #e0e0e0; - background-image: -webkit-radial-gradient(circle, #e0e0e0 10%, #ffffff 11%); - background-image: -o-radial-gradient(circle, #e0e0e0 10%, #ffffff 11%); - background-image: radial-gradient(circle, #e0e0e0 10%, #ffffff 11%); - background-repeat: no-repeat; - -webkit-background-size: 1000% 1000%; - background-size: 1000% 1000%; - -webkit-box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4); - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4); -} -.btn-primary { - -webkit-background-size: 200% 200%; - background-size: 200% 200%; - background-position: 50%; -} -.btn-primary:focus { - background-color: #2196f3; -} -.btn-primary:hover, -.btn-primary:active:hover { - background-color: #0d87e9; -} -.btn-primary:active { - background-color: #0b76cc; - background-image: -webkit-radial-gradient(circle, #0b76cc 10%, #2196f3 11%); - background-image: -o-radial-gradient(circle, #0b76cc 10%, #2196f3 11%); - background-image: radial-gradient(circle, #0b76cc 10%, #2196f3 11%); - background-repeat: no-repeat; - -webkit-background-size: 1000% 1000%; - background-size: 1000% 1000%; - -webkit-box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4); - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4); -} -.btn-success { - -webkit-background-size: 200% 200%; - background-size: 200% 200%; - background-position: 50%; -} -.btn-success:focus { - background-color: #4caf50; -} -.btn-success:hover, -.btn-success:active:hover { - background-color: #439a46; -} -.btn-success:active { - background-color: #39843c; - background-image: -webkit-radial-gradient(circle, #39843c 10%, #4caf50 11%); - background-image: -o-radial-gradient(circle, #39843c 10%, #4caf50 11%); - background-image: radial-gradient(circle, #39843c 10%, #4caf50 11%); - background-repeat: no-repeat; - -webkit-background-size: 1000% 1000%; - background-size: 1000% 1000%; - -webkit-box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4); - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4); -} -.btn-info { - -webkit-background-size: 200% 200%; - background-size: 200% 200%; - background-position: 50%; -} -.btn-info:focus { - background-color: #9c27b0; -} -.btn-info:hover, -.btn-info:active:hover { - background-color: #862197; -} -.btn-info:active { - background-color: #701c7e; - background-image: -webkit-radial-gradient(circle, #701c7e 10%, #9c27b0 11%); - background-image: -o-radial-gradient(circle, #701c7e 10%, #9c27b0 11%); - background-image: radial-gradient(circle, #701c7e 10%, #9c27b0 11%); - background-repeat: no-repeat; - -webkit-background-size: 1000% 1000%; - background-size: 1000% 1000%; - -webkit-box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4); - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4); -} -.btn-warning { - -webkit-background-size: 200% 200%; - background-size: 200% 200%; - background-position: 50%; -} -.btn-warning:focus { - background-color: #ff9800; -} -.btn-warning:hover, -.btn-warning:active:hover { - background-color: #e08600; -} -.btn-warning:active { - background-color: #c27400; - background-image: -webkit-radial-gradient(circle, #c27400 10%, #ff9800 11%); - background-image: -o-radial-gradient(circle, #c27400 10%, #ff9800 11%); - background-image: radial-gradient(circle, #c27400 10%, #ff9800 11%); - background-repeat: no-repeat; - -webkit-background-size: 1000% 1000%; - background-size: 1000% 1000%; - -webkit-box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4); - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4); -} -.btn-danger { - -webkit-background-size: 200% 200%; - background-size: 200% 200%; - background-position: 50%; -} -.btn-danger:focus { - background-color: #e51c23; -} -.btn-danger:hover, -.btn-danger:active:hover { - background-color: #cb171e; -} -.btn-danger:active { - background-color: #b0141a; - background-image: -webkit-radial-gradient(circle, #b0141a 10%, #e51c23 11%); - background-image: -o-radial-gradient(circle, #b0141a 10%, #e51c23 11%); - background-image: radial-gradient(circle, #b0141a 10%, #e51c23 11%); - background-repeat: no-repeat; - -webkit-background-size: 1000% 1000%; - background-size: 1000% 1000%; - -webkit-box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4); - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4); -} -.btn-link { - -webkit-background-size: 200% 200%; - background-size: 200% 200%; - background-position: 50%; -} -.btn-link:focus { - background-color: #ffffff; -} -.btn-link:hover, -.btn-link:active:hover { - background-color: #f0f0f0; -} -.btn-link:active { - background-color: #e0e0e0; - background-image: -webkit-radial-gradient(circle, #e0e0e0 10%, #ffffff 11%); - background-image: -o-radial-gradient(circle, #e0e0e0 10%, #ffffff 11%); - background-image: radial-gradient(circle, #e0e0e0 10%, #ffffff 11%); - background-repeat: no-repeat; - -webkit-background-size: 1000% 1000%; - background-size: 1000% 1000%; - -webkit-box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4); - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4); -} -.btn { - text-transform: uppercase; - border: none; - -webkit-box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.4); - box-shadow: 1px 1px 4px rgba(0, 0, 0, 0.4); - -webkit-transition: all 0.4s; - -o-transition: all 0.4s; - transition: all 0.4s; -} -.btn-link { - border-radius: 3px; - -webkit-box-shadow: none; - box-shadow: none; - color: #444444; -} -.btn-link:hover, -.btn-link:focus { - -webkit-box-shadow: none; - box-shadow: none; - color: #444444; - text-decoration: none; -} -.btn-default.disabled { - background-color: rgba(0, 0, 0, 0.1); - color: rgba(0, 0, 0, 0.4); - opacity: 1; -} -.btn-group .btn + .btn, -.btn-group .btn + .btn-group, -.btn-group .btn-group + .btn, -.btn-group .btn-group + .btn-group { - margin-left: 0; -} -.btn-group-vertical > .btn + .btn, -.btn-group-vertical > .btn + .btn-group, -.btn-group-vertical > .btn-group + .btn, -.btn-group-vertical > .btn-group + .btn-group { - margin-top: 0; -} -body { - -webkit-font-smoothing: antialiased; - letter-spacing: .1px; -} -p { - margin: 0 0 1em; -} -input, -button { - -webkit-font-smoothing: antialiased; - letter-spacing: .1px; -} -a { - -webkit-transition: all 0.2s; - -o-transition: all 0.2s; - transition: all 0.2s; -} -.table-hover > tbody > tr, -.table-hover > tbody > tr > th, -.table-hover > tbody > tr > td { - -webkit-transition: all 0.2s; - -o-transition: all 0.2s; - transition: all 0.2s; -} -label { - font-weight: normal; -} -textarea, -textarea.form-control, -input.form-control, -input[type=text], -input[type=password], -input[type=email], -input[type=number], -[type=text].form-control, -[type=password].form-control, -[type=email].form-control, -[type=tel].form-control, -[contenteditable].form-control { - padding: 0; - border: none; - border-radius: 0; - -webkit-appearance: none; - -webkit-box-shadow: inset 0 -1px 0 #dddddd; - box-shadow: inset 0 -1px 0 #dddddd; - font-size: 16px; -} -textarea:focus, -textarea.form-control:focus, -input.form-control:focus, -input[type=text]:focus, -input[type=password]:focus, -input[type=email]:focus, -input[type=number]:focus, -[type=text].form-control:focus, -[type=password].form-control:focus, -[type=email].form-control:focus, -[type=tel].form-control:focus, -[contenteditable].form-control:focus { - -webkit-box-shadow: inset 0 -2px 0 #2196f3; - box-shadow: inset 0 -2px 0 #2196f3; -} -textarea[disabled], -textarea.form-control[disabled], -input.form-control[disabled], -input[type=text][disabled], -input[type=password][disabled], -input[type=email][disabled], -input[type=number][disabled], -[type=text].form-control[disabled], -[type=password].form-control[disabled], -[type=email].form-control[disabled], -[type=tel].form-control[disabled], -[contenteditable].form-control[disabled], -textarea[readonly], -textarea.form-control[readonly], -input.form-control[readonly], -input[type=text][readonly], -input[type=password][readonly], -input[type=email][readonly], -input[type=number][readonly], -[type=text].form-control[readonly], -[type=password].form-control[readonly], -[type=email].form-control[readonly], -[type=tel].form-control[readonly], -[contenteditable].form-control[readonly] { - -webkit-box-shadow: none; - box-shadow: none; - border-bottom: 1px dotted #ddd; -} -textarea.input-sm, -textarea.form-control.input-sm, -input.form-control.input-sm, -input[type=text].input-sm, -input[type=password].input-sm, -input[type=email].input-sm, -input[type=number].input-sm, -[type=text].form-control.input-sm, -[type=password].form-control.input-sm, -[type=email].form-control.input-sm, -[type=tel].form-control.input-sm, -[contenteditable].form-control.input-sm { - font-size: 12px; -} -textarea.input-lg, -textarea.form-control.input-lg, -input.form-control.input-lg, -input[type=text].input-lg, -input[type=password].input-lg, -input[type=email].input-lg, -input[type=number].input-lg, -[type=text].form-control.input-lg, -[type=password].form-control.input-lg, -[type=email].form-control.input-lg, -[type=tel].form-control.input-lg, -[contenteditable].form-control.input-lg { - font-size: 17px; -} -select, -select.form-control { - border: 0; - border-radius: 0; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - padding-left: 0; - padding-right: 0\9; - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAMAAACelLz8AAAAJ1BMVEVmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmaP/QSjAAAADHRSTlMAAgMJC0uWpKa6wMxMdjkoAAAANUlEQVR4AeXJyQEAERAAsNl7Hf3X6xt0QL6JpZWq30pdvdadme+0PMdzvHm8YThHcT1H7K0BtOMDniZhWOgAAAAASUVORK5CYII=); - -webkit-background-size: 13px 13px; - background-size: 13px; - background-repeat: no-repeat; - background-position: right center; - -webkit-box-shadow: inset 0 -1px 0 #dddddd; - box-shadow: inset 0 -1px 0 #dddddd; - font-size: 16px; - line-height: 1.5; -} -select::-ms-expand, -select.form-control::-ms-expand { - display: none; -} -select.input-sm, -select.form-control.input-sm { - font-size: 12px; -} -select.input-lg, -select.form-control.input-lg { - font-size: 17px; -} -select:focus, -select.form-control:focus { - -webkit-box-shadow: inset 0 -2px 0 #2196f3; - box-shadow: inset 0 -2px 0 #2196f3; - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAaCAMAAACelLz8AAAAJ1BMVEUhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISEhISF8S9ewAAAADHRSTlMAAgMJC0uWpKa6wMxMdjkoAAAANUlEQVR4AeXJyQEAERAAsNl7Hf3X6xt0QL6JpZWq30pdvdadme+0PMdzvHm8YThHcT1H7K0BtOMDniZhWOgAAAAASUVORK5CYII=); -} -select[multiple], -select.form-control[multiple] { - background: none; -} -.radio label, -.radio-inline label, -.checkbox label, -.checkbox-inline label { - padding-left: 25px; -} -.radio input[type="radio"], -.radio-inline input[type="radio"], -.checkbox input[type="radio"], -.checkbox-inline input[type="radio"], -.radio input[type="checkbox"], -.radio-inline input[type="checkbox"], -.checkbox input[type="checkbox"], -.checkbox-inline input[type="checkbox"] { - margin-left: -25px; -} -input[type="radio"], -.radio input[type="radio"], -.radio-inline input[type="radio"] { - position: relative; - margin-top: 6px; - margin-right: 4px; - vertical-align: top; - border: none; - background-color: transparent; - -webkit-appearance: none; - appearance: none; - cursor: pointer; -} -input[type="radio"]:focus, -.radio input[type="radio"]:focus, -.radio-inline input[type="radio"]:focus { - outline: none; -} -input[type="radio"]:before, -.radio input[type="radio"]:before, -.radio-inline input[type="radio"]:before, -input[type="radio"]:after, -.radio input[type="radio"]:after, -.radio-inline input[type="radio"]:after { - content: ""; - display: block; - width: 18px; - height: 18px; - border-radius: 50%; - -webkit-transition: 240ms; - -o-transition: 240ms; - transition: 240ms; -} -input[type="radio"]:before, -.radio input[type="radio"]:before, -.radio-inline input[type="radio"]:before { - position: absolute; - left: 0; - top: -3px; - background-color: #2196f3; - -webkit-transform: scale(0); - -ms-transform: scale(0); - -o-transform: scale(0); - transform: scale(0); -} -input[type="radio"]:after, -.radio input[type="radio"]:after, -.radio-inline input[type="radio"]:after { - position: relative; - top: -3px; - border: 2px solid #666666; -} -input[type="radio"]:checked:before, -.radio input[type="radio"]:checked:before, -.radio-inline input[type="radio"]:checked:before { - -webkit-transform: scale(0.5); - -ms-transform: scale(0.5); - -o-transform: scale(0.5); - transform: scale(0.5); -} -input[type="radio"]:disabled:checked:before, -.radio input[type="radio"]:disabled:checked:before, -.radio-inline input[type="radio"]:disabled:checked:before { - background-color: #bbbbbb; -} -input[type="radio"]:checked:after, -.radio input[type="radio"]:checked:after, -.radio-inline input[type="radio"]:checked:after { - border-color: #2196f3; -} -input[type="radio"]:disabled:after, -.radio input[type="radio"]:disabled:after, -.radio-inline input[type="radio"]:disabled:after, -input[type="radio"]:disabled:checked:after, -.radio input[type="radio"]:disabled:checked:after, -.radio-inline input[type="radio"]:disabled:checked:after { - border-color: #bbbbbb; -} -input[type="checkbox"], -.checkbox input[type="checkbox"], -.checkbox-inline input[type="checkbox"] { - position: relative; - border: none; - margin-bottom: -4px; - -webkit-appearance: none; - appearance: none; - cursor: pointer; -} -input[type="checkbox"]:focus, -.checkbox input[type="checkbox"]:focus, -.checkbox-inline input[type="checkbox"]:focus { - outline: none; -} -input[type="checkbox"]:focus:after, -.checkbox input[type="checkbox"]:focus:after, -.checkbox-inline input[type="checkbox"]:focus:after { - border-color: #2196f3; -} -input[type="checkbox"]:after, -.checkbox input[type="checkbox"]:after, -.checkbox-inline input[type="checkbox"]:after { - content: ""; - display: block; - width: 18px; - height: 18px; - margin-top: -2px; - margin-right: 5px; - border: 2px solid #666666; - border-radius: 2px; - -webkit-transition: 240ms; - -o-transition: 240ms; - transition: 240ms; -} -input[type="checkbox"]:checked:before, -.checkbox input[type="checkbox"]:checked:before, -.checkbox-inline input[type="checkbox"]:checked:before { - content: ""; - position: absolute; - top: 0; - left: 6px; - display: table; - width: 6px; - height: 12px; - border: 2px solid #fff; - border-top-width: 0; - border-left-width: 0; - -webkit-transform: rotate(45deg); - -ms-transform: rotate(45deg); - -o-transform: rotate(45deg); - transform: rotate(45deg); -} -input[type="checkbox"]:checked:after, -.checkbox input[type="checkbox"]:checked:after, -.checkbox-inline input[type="checkbox"]:checked:after { - background-color: #2196f3; - border-color: #2196f3; -} -input[type="checkbox"]:disabled:after, -.checkbox input[type="checkbox"]:disabled:after, -.checkbox-inline input[type="checkbox"]:disabled:after { - border-color: #bbbbbb; -} -input[type="checkbox"]:disabled:checked:after, -.checkbox input[type="checkbox"]:disabled:checked:after, -.checkbox-inline input[type="checkbox"]:disabled:checked:after { - background-color: #bbbbbb; - border-color: transparent; -} -.has-warning input:not([type=checkbox]), -.has-warning .form-control, -.has-warning input.form-control[readonly], -.has-warning input[type=text][readonly], -.has-warning [type=text].form-control[readonly], -.has-warning input:not([type=checkbox]):focus, -.has-warning .form-control:focus { - border-bottom: none; - -webkit-box-shadow: inset 0 -2px 0 #ff9800; - box-shadow: inset 0 -2px 0 #ff9800; -} -.has-error input:not([type=checkbox]), -.has-error .form-control, -.has-error input.form-control[readonly], -.has-error input[type=text][readonly], -.has-error [type=text].form-control[readonly], -.has-error input:not([type=checkbox]):focus, -.has-error .form-control:focus { - border-bottom: none; - -webkit-box-shadow: inset 0 -2px 0 #e51c23; - box-shadow: inset 0 -2px 0 #e51c23; -} -.has-success input:not([type=checkbox]), -.has-success .form-control, -.has-success input.form-control[readonly], -.has-success input[type=text][readonly], -.has-success [type=text].form-control[readonly], -.has-success input:not([type=checkbox]):focus, -.has-success .form-control:focus { - border-bottom: none; - -webkit-box-shadow: inset 0 -2px 0 #4caf50; - box-shadow: inset 0 -2px 0 #4caf50; -} -.has-warning .input-group-addon, -.has-error .input-group-addon, -.has-success .input-group-addon { - color: #666666; - border-color: transparent; - background-color: transparent; -} -.form-group-lg select, -.form-group-lg select.form-control { - line-height: 1.5; -} -.nav-tabs > li > a, -.nav-tabs > li > a:focus { - margin-right: 0; - background-color: transparent; - border: none; - color: #666666; - -webkit-box-shadow: inset 0 -1px 0 #dddddd; - box-shadow: inset 0 -1px 0 #dddddd; - -webkit-transition: all 0.2s; - -o-transition: all 0.2s; - transition: all 0.2s; -} -.nav-tabs > li > a:hover, -.nav-tabs > li > a:focus:hover { - background-color: transparent; - -webkit-box-shadow: inset 0 -2px 0 #2196f3; - box-shadow: inset 0 -2px 0 #2196f3; - color: #2196f3; -} -.nav-tabs > li.active > a, -.nav-tabs > li.active > a:focus { - border: none; - -webkit-box-shadow: inset 0 -2px 0 #2196f3; - box-shadow: inset 0 -2px 0 #2196f3; - color: #2196f3; -} -.nav-tabs > li.active > a:hover, -.nav-tabs > li.active > a:focus:hover { - border: none; - color: #2196f3; -} -.nav-tabs > li.disabled > a { - -webkit-box-shadow: inset 0 -1px 0 #dddddd; - box-shadow: inset 0 -1px 0 #dddddd; -} -.nav-tabs.nav-justified > li > a, -.nav-tabs.nav-justified > li > a:hover, -.nav-tabs.nav-justified > li > a:focus, -.nav-tabs.nav-justified > .active > a, -.nav-tabs.nav-justified > .active > a:hover, -.nav-tabs.nav-justified > .active > a:focus { - border: none; -} -.nav-tabs .dropdown-menu { - margin-top: 0; -} -.dropdown-menu { - margin-top: 0; - border: none; - -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); -} -.alert { - border: none; - color: #fff; -} -.alert-success { - background-color: #4caf50; -} -.alert-info { - background-color: #9c27b0; -} -.alert-warning { - background-color: #ff9800; -} -.alert-danger { - background-color: #e51c23; -} -.alert a:not(.close):not(.btn), -.alert .alert-link { - color: #fff; - font-weight: bold; -} -.alert .close { - color: #fff; -} -.badge { - padding: 4px 6px 4px; -} -.progress { - position: relative; - z-index: 1; - height: 6px; - border-radius: 0; - -webkit-box-shadow: none; - box-shadow: none; -} -.progress-bar { - -webkit-box-shadow: none; - box-shadow: none; -} -.progress-bar:last-child { - border-radius: 0 3px 3px 0; -} -.progress-bar:last-child:before { - display: block; - content: ""; - position: absolute; - width: 100%; - height: 100%; - left: 0; - right: 0; - z-index: -1; - background-color: #cae6fc; -} -.progress-bar-success:last-child.progress-bar:before { - background-color: #c7e7c8; -} -.progress-bar-info:last-child.progress-bar:before { - background-color: #edc9f3; -} -.progress-bar-warning:last-child.progress-bar:before { - background-color: #ffe0b3; -} -.progress-bar-danger:last-child.progress-bar:before { - background-color: #f28e92; -} -.close { - font-size: 34px; - font-weight: 300; - line-height: 24px; - opacity: 0.6; - -webkit-transition: all 0.2s; - -o-transition: all 0.2s; - transition: all 0.2s; -} -.close:hover { - opacity: 1; -} -.list-group-item { - padding: 15px; -} -.list-group-item-text { - color: #bbbbbb; -} -.well { - border-radius: 0; - -webkit-box-shadow: none; - box-shadow: none; -} -.panel { - border: none; - border-radius: 2px; - -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); -} -.panel-heading { - border-bottom: none; -} -.panel-footer { - border-top: none; -} -.popover { - border: none; - -webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3); -} -.carousel-caption h1, -.carousel-caption h2, -.carousel-caption h3, -.carousel-caption h4, -.carousel-caption h5, -.carousel-caption h6 { - color: inherit; -} diff --git a/app/index.html b/app/index.html deleted file mode 100644 index 40d91b9..0000000 --- a/app/index.html +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - Zimit — Create a zim archive out of a website URL - - - - -
-
-
-
-
- - -
-
- - -
-
- - -
-
-
-

- -

-
-

- This is a Zim creator. Enter the url of the website you want ton turn in a zim file, a title and click on Create zim File -

-

Enjoy !

-
- - - - diff --git a/favicon.ico b/favicon.ico deleted file mode 100644 index 6054bae..0000000 Binary files a/favicon.ico and /dev/null differ diff --git a/offliner-definition.json b/offliner-definition.json new file mode 100644 index 0000000..4bb68b5 --- /dev/null +++ b/offliner-definition.json @@ -0,0 +1,981 @@ +{ + "offliner_id": "zimit", + "stdOutput": true, + "stdStats": "zimit-progress-file", + "flags": { + "seeds": { + "type": "string", + "required": false, + "title": "Seeds", + "description": "The seed URL(s) to start crawling from. Multile seed URL must be separated by a comma (usually not needed, these are just the crawl seeds). First seed URL is used as ZIM homepage" + }, + "seed_file": { + "type": "string", + "required": false, + "title": "Seed File", + "description": "If set, read a list of seed urls, one per line. HTTPS URL to an online file." + }, + "lang": { + "type": "string", + "required": false, + "title": "Browser Language", + "description": "If set, sets the language used by the browser, should be ISO 639 language[-country] code, e.g. `en` or `en-GB`" + }, + "title": { + "type": "string", + "required": false, + "title": "Title", + "description": "Custom title for your ZIM. Defaults to title of main page", + "minLength": 1, + "maxLength": 30 + }, + "description": { + "type": "string", + "required": false, + "title": "Description", + "description": "Description for ZIM", + "minLength": 1, + "maxLength": 80 + }, + "favicon": { + "type": "blob", + "kind": "image", + "required": false, + "title": "Illustration", + "description": "URL for Illustration. " + }, + "tags": { + "type": "string", + "required": false, + "title": "ZIM Tags", + "description": "Single string with individual tags separated by a semicolon." + }, + "creator": { + "type": "string", + "required": false, + "title": "Creator", + "description": "Name of content creator" + }, + "publisher": { + "type": "string", + "required": false, + "title": "Publisher", + "isPublisher": true, + "description": "Custom publisher name (ZIM metadata). openZIM otherwise" + }, + "source": { + "type": "string", + "required": false, + "title": "Source", + "description": "Source name/URL of content" + }, + "workers": { + "type": "integer", + "required": false, + "title": "Workers", + "description": "The number of workers to run in parallel. Defaults to 1", + "min": 1 + }, + "wait_until": { + "type": "string", + "required": false, + "title": "WaitUntil", + "description": "Puppeteer page.goto() condition to wait for before continuing. One of load, domcontentloaded, networkidle0 or networkidle2, or a comma-separated combination of those. Default is load,networkidle2" + }, + "extra_hops": { + "type": "integer", + "required": false, + "title": "Extra Hops", + "description": "Number of extra 'hops' to follow, beyond the current scope. Default is 0", + "min": 0 + }, + "page_limit": { + "type": "integer", + "required": false, + "title": "Page Limit", + "description": "Limit crawl to this number of pages. Default is 0 (no-limit).", + "min": 0 + }, + "max_page_limit": { + "type": "integer", + "required": false, + "title": "Max Page Limit", + "description": "Maximum pages to crawl, overriding pageLimit if both are set. Default is 0 (no-limit)", + "min": 0 + }, + "page_load_timeout": { + "type": "integer", + "required": false, + "title": "Page Load Timeout", + "description": "Timeout for each page to load (in seconds). Default is 90", + "min": 0 + }, + "scope_type": { + "type": "string-enum", + "required": false, + "title": "Scope Type", + "description": "A predfined scope of the crawl. For more customization, use 'custom' and set scopeIncludeRx/scopeExcludeRx regexes. Default is custom if scopeIncludeRx is set, prefix otherwise.", + "choices": [ + { + "title": "Page", + "value": "page" + }, + { + "title": "Page SPA", + "value": "page-spa" + }, + { + "title": "Prefix", + "value": "prefix" + }, + { + "title": "Host", + "value": "host" + }, + { + "title": "Domain", + "value": "domain" + }, + { + "title": "Any", + "value": "any" + }, + { + "title": "Custom", + "value": "custom" + } + ] + }, + "scope_include_rx": { + "type": "string", + "required": false, + "title": "Scope Include Regex", + "description": "Regex of page URLs that should be included in the crawl (defaults to the immediate directory of seed)" + }, + "scope_exclude_rx": { + "type": "string", + "required": false, + "title": "Scope Exclude Regex", + "description": "Regex of page URLs that should be excluded from the crawl" + }, + "allow_hash_urls": { + "type": "boolean", + "required": false, + "title": "Allow Hashtag URLs", + "description": "Allow Hashtag URLs, useful for single-page-application crawling or when different hashtags load dynamic content" + }, + "mobile_device": { + "type": "string-enum", + "required": false, + "title": "As device", + "description": "Device to crawl as. See Pupeeter's Device.ts for a list", + "choices": [ + { + "title": "Blackberry Playbook", + "value": "Blackberry PlayBook" + }, + { + "title": "Blackberry Playbook Landscape", + "value": "Blackberry PlayBook landscape" + }, + { + "title": "Blackberry Z30", + "value": "BlackBerry Z30" + }, + { + "title": "Blackberry Z30 Landscape", + "value": "BlackBerry Z30 landscape" + }, + { + "title": "Galaxy Note 3", + "value": "Galaxy Note 3" + }, + { + "title": "Galaxy Note 3 Landscape", + "value": "Galaxy Note 3 landscape" + }, + { + "title": "Galaxy Note II", + "value": "Galaxy Note II" + }, + { + "title": "Galaxy Note II Landscape", + "value": "Galaxy Note II landscape" + }, + { + "title": "Galaxy S III", + "value": "Galaxy S III" + }, + { + "title": "Galaxy S III Landscape", + "value": "Galaxy S III landscape" + }, + { + "title": "Galaxy S5", + "value": "Galaxy S5" + }, + { + "title": "Galaxy S5 Landscape", + "value": "Galaxy S5 landscape" + }, + { + "title": "Galaxy S8", + "value": "Galaxy S8" + }, + { + "title": "Galaxy S8 Landscape", + "value": "Galaxy S8 landscape" + }, + { + "title": "Galaxy S9 Plus", + "value": "Galaxy S9+" + }, + { + "title": "Galaxy S9 Plus Landscape", + "value": "Galaxy S9+ landscape" + }, + { + "title": "Galaxy Tab S4", + "value": "Galaxy Tab S4" + }, + { + "title": "Galaxy Tab S4 Landscape", + "value": "Galaxy Tab S4 landscape" + }, + { + "title": "iPad", + "value": "iPad" + }, + { + "title": "iPad Landscape", + "value": "iPad landscape" + }, + { + "title": "iPad Gen 6", + "value": "iPad (gen 6)" + }, + { + "title": "iPad Gen 6 Landscape", + "value": "iPad (gen 6) landscape" + }, + { + "title": "iPad Gen 7", + "value": "iPad (gen 7)" + }, + { + "title": "iPad Gen 7 Landscape", + "value": "iPad (gen 7) landscape" + }, + { + "title": "iPad Mini", + "value": "iPad Mini" + }, + { + "title": "iPad Mini Landscape", + "value": "iPad Mini landscape" + }, + { + "title": "iPad Pro", + "value": "iPad Pro" + }, + { + "title": "iPad Pro Landscape", + "value": "iPad Pro landscape" + }, + { + "title": "iPad Pro 11", + "value": "iPad Pro 11" + }, + { + "title": "iPad Pro 11 Landscape", + "value": "iPad Pro 11 landscape" + }, + { + "title": "iPhone 4", + "value": "iPhone 4" + }, + { + "title": "iPhone 4 Landscape", + "value": "iPhone 4 landscape" + }, + { + "title": "iPhone 5", + "value": "iPhone 5" + }, + { + "title": "iPhone 5 Landscape", + "value": "iPhone 5 landscape" + }, + { + "title": "iPhone 6", + "value": "iPhone 6" + }, + { + "title": "iPhone 6 Landscape", + "value": "iPhone 6 landscape" + }, + { + "title": "iPhone 6 Plus", + "value": "iPhone 6 Plus" + }, + { + "title": "iPhone 6 Plus Landscape", + "value": "iPhone 6 Plus landscape" + }, + { + "title": "iPhone 7", + "value": "iPhone 7" + }, + { + "title": "iPhone 7 Landscape", + "value": "iPhone 7 landscape" + }, + { + "title": "iPhone 7 Plus", + "value": "iPhone 7 Plus" + }, + { + "title": "iPhone 7 Plus Landscape", + "value": "iPhone 7 Plus landscape" + }, + { + "title": "iPhone 8", + "value": "iPhone 8" + }, + { + "title": "iPhone 8 Landscape", + "value": "iPhone 8 landscape" + }, + { + "title": "iPhone 8 Plus", + "value": "iPhone 8 Plus" + }, + { + "title": "iPhone 8 Plus Landscape", + "value": "iPhone 8 Plus landscape" + }, + { + "title": "iPhone SE", + "value": "iPhone SE" + }, + { + "title": "iPhone SE Landscape", + "value": "iPhone SE landscape" + }, + { + "title": "iPhone X", + "value": "iPhone X" + }, + { + "title": "iPhone X Landscape", + "value": "iPhone X landscape" + }, + { + "title": "iPhone XR", + "value": "iPhone XR" + }, + { + "title": "iPhone XR Landscape", + "value": "iPhone XR landscape" + }, + { + "title": "iPhone 11", + "value": "iPhone 11" + }, + { + "title": "iPhone 11 Landscape", + "value": "iPhone 11 landscape" + }, + { + "title": "iPhone 11 Pro", + "value": "iPhone 11 Pro" + }, + { + "title": "iPhone 11 Pro Landscape", + "value": "iPhone 11 Pro landscape" + }, + { + "title": "iPhone 11 Pro Max", + "value": "iPhone 11 Pro Max" + }, + { + "title": "iPhone 11 Pro Max Landscape", + "value": "iPhone 11 Pro Max landscape" + }, + { + "title": "iPhone 12", + "value": "iPhone 12" + }, + { + "title": "iPhone 12 Landscape", + "value": "iPhone 12 landscape" + }, + { + "title": "iPhone 12 Pro", + "value": "iPhone 12 Pro" + }, + { + "title": "iPhone 12 Pro Landscape", + "value": "iPhone 12 Pro landscape" + }, + { + "title": "iPhone 12 Pro Max", + "value": "iPhone 12 Pro Max" + }, + { + "title": "iPhone 12 Pro Max Landscape", + "value": "iPhone 12 Pro Max landscape" + }, + { + "title": "iPhone 12 Mini", + "value": "iPhone 12 Mini" + }, + { + "title": "iPhone 12 Mini Landscape", + "value": "iPhone 12 Mini landscape" + }, + { + "title": "iPhone 13", + "value": "iPhone 13" + }, + { + "title": "iPhone 13 Landscape", + "value": "iPhone 13 landscape" + }, + { + "title": "iPhone 13 Pro", + "value": "iPhone 13 Pro" + }, + { + "title": "iPhone 13 Pro Landscape", + "value": "iPhone 13 Pro landscape" + }, + { + "title": "iPhone 13 Pro Max", + "value": "iPhone 13 Pro Max" + }, + { + "title": "iPhone 13 Pro Max Landscape", + "value": "iPhone 13 Pro Max landscape" + }, + { + "title": "iPhone 13 Mini", + "value": "iPhone 13 Mini" + }, + { + "title": "iPhone 13 Mini Landscape", + "value": "iPhone 13 Mini landscape" + }, + { + "title": "Jio Phone 2", + "value": "JioPhone 2" + }, + { + "title": "Jio Phone 2 Landscape", + "value": "JioPhone 2 landscape" + }, + { + "title": "Kindle Fire HDX", + "value": "Kindle Fire HDX" + }, + { + "title": "Kindle Fire HDX Landscape", + "value": "Kindle Fire HDX landscape" + }, + { + "title": "LG Optimus L70", + "value": "LG Optimus L70" + }, + { + "title": "LG Optimus L70 Landscape", + "value": "LG Optimus L70 landscape" + }, + { + "title": "Microsoft Lumia 550", + "value": "Microsoft Lumia 550" + }, + { + "title": "Microsoft Lumia 950", + "value": "Microsoft Lumia 950" + }, + { + "title": "Microsoft Lumia 950 Landscape", + "value": "Microsoft Lumia 950 landscape" + }, + { + "title": "Nexus 10", + "value": "Nexus 10" + }, + { + "title": "Nexus 10 Landscape", + "value": "Nexus 10 landscape" + }, + { + "title": "Nexus 4", + "value": "Nexus 4" + }, + { + "title": "Nexus 4 Landscape", + "value": "Nexus 4 landscape" + }, + { + "title": "Nexus 5", + "value": "Nexus 5" + }, + { + "title": "Nexus 5 Landscape", + "value": "Nexus 5 landscape" + }, + { + "title": "Nexus 5X", + "value": "Nexus 5X" + }, + { + "title": "Nexus 5X Landscape", + "value": "Nexus 5X landscape" + }, + { + "title": "Nexus 6", + "value": "Nexus 6" + }, + { + "title": "Nexus 6 Landscape", + "value": "Nexus 6 landscape" + }, + { + "title": "Nexus 6P", + "value": "Nexus 6P" + }, + { + "title": "Nexus 6P Landscape", + "value": "Nexus 6P landscape" + }, + { + "title": "Nexus 7", + "value": "Nexus 7" + }, + { + "title": "Nexus 7 Landscape", + "value": "Nexus 7 landscape" + }, + { + "title": "Nokia Lumia 520", + "value": "Nokia Lumia 520" + }, + { + "title": "Nokia Lumia 520 Landscape", + "value": "Nokia Lumia 520 landscape" + }, + { + "title": "Nokia N9", + "value": "Nokia N9" + }, + { + "title": "Nokia N9 Landscape", + "value": "Nokia N9 landscape" + }, + { + "title": "Pixel 2", + "value": "Pixel 2" + }, + { + "title": "Pixel 2 Landscape", + "value": "Pixel 2 landscape" + }, + { + "title": "Pixel 2 XL", + "value": "Pixel 2 XL" + }, + { + "title": "Pixel 2 XL Landscape", + "value": "Pixel 2 XL landscape" + }, + { + "title": "Pixel 3", + "value": "Pixel 3" + }, + { + "title": "Pixel 3 Landscape", + "value": "Pixel 3 landscape" + }, + { + "title": "Pixel 4", + "value": "Pixel 4" + }, + { + "title": "Pixel 4 Landscape", + "value": "Pixel 4 landscape" + }, + { + "title": "Pixel 4A 5G", + "value": "Pixel 4a (5G)" + }, + { + "title": "Pixel 4A 5G Landscape", + "value": "Pixel 4a (5G) landscape" + }, + { + "title": "Pixel 5", + "value": "Pixel 5" + }, + { + "title": "Pixel 5 Landscape", + "value": "Pixel 5 landscape" + }, + { + "title": "Moto G4", + "value": "Moto G4" + }, + { + "title": "Moto G4 Landscape", + "value": "Moto G4 landscape" + } + ] + }, + "select_links": { + "type": "string", + "required": false, + "title": "Select Links", + "description": "One or more selectors for extracting links, in the format [css selector]->[property to use],[css selector]->@[attribute to use]" + }, + "click_selector": { + "type": "string", + "required": false, + "title": "Click Selector", + "description": "Selector for elements to click when using the autoclick behavior. Default is 'a'" + }, + "block_rules": { + "type": "string", + "required": false, + "title": "Block Rules", + "description": "Additional rules for blocking certain URLs from being loaded, by URL regex and optionally via text match in an iframe" + }, + "block_message": { + "type": "string", + "required": false, + "title": "Block Message", + "description": "If specified, when a URL is blocked, a record with this error message is added instead" + }, + "block_ads": { + "type": "boolean", + "required": false, + "title": "Block Ads", + "description": "If set, block advertisements from being loaded (based on Stephen Black's blocklist). Note that some bad domains are also blocked by zimit configuration even if this option is not set." + }, + "ad_block_message": { + "type": "string", + "required": false, + "title": "Ads Block Message", + "description": "If specified, when an ad is blocked, a record with this error message is added instead" + }, + "user_agent": { + "type": "string", + "required": false, + "title": "User Agent", + "description": "Override user-agent with specified" + }, + "user_agent_suffix": { + "type": "string", + "required": false, + "title": "User Agent Suffix", + "description": "Append suffix to existing browser user-agent. Defaults to +Zimit" + }, + "use_sitemap": { + "type": "string", + "required": false, + "title": "Sitemap URL", + "description": "Use as sitemap to get additional URLs for the crawl (usually at /sitemap.xml)" + }, + "sitemap_from_date": { + "type": "string", + "required": false, + "title": "Sitemap From Date", + "description": "If set, filter URLs from sitemaps to those greater than or equal to (>=) provided ISO Date string (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS or partial date)" + }, + "sitemap_to_date": { + "type": "string", + "required": false, + "title": "Sitemap To Date", + "description": "If set, filter URLs from sitemaps to those less than or equal to (<=) provided ISO Date string (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS or partial date)" + }, + "behavior_timeout": { + "type": "integer", + "required": false, + "title": "Behavior Timeout", + "description": "If >0, timeout (in seconds) for in-page behavior will run on each page. If 0, a behavior can run until finish. Default is 90.", + "min": 0 + }, + "post_load_delay": { + "type": "integer", + "required": false, + "title": "Post Load Delay", + "description": "If >0, amount of time to sleep (in seconds) after page has loaded, before taking screenshots / getting text / running behaviors. Default is 0.", + "min": 0 + }, + "page_extra_delay": { + "type": "integer", + "required": false, + "title": "Page Extra Delay", + "description": "If >0, amount of time to sleep (in seconds) after behaviors before moving on to next page. Default is 0.", + "min": 0 + }, + "dedup_policy": { + "type": "string-enum", + "required": false, + "title": "Dedup Policy", + "description": "Deduplication policy. One of skip, revisit or keep. Default is skip", + "choices": [ + { + "title": "Skip", + "value": "skip" + }, + { + "title": "Revisit", + "value": "revisit" + }, + { + "title": "Keep", + "value": "keep" + } + ] + }, + "screenshot": { + "type": "string", + "required": false, + "title": "Screenshot", + "description": "Screenshot options for crawler. One of view, thumbnail, fullPage, fullPageFinal or a comma-separated combination of those." + }, + "size_soft_limit": { + "type": "integer", + "required": false, + "title": "Size Soft Limit", + "description": "If set, save crawl state and stop crawl if WARC size exceeds this value. ZIM will still be created.", + "min": 0 + }, + "size_hard_limit": { + "type": "integer", + "required": false, + "title": "Size Hard Limit", + "description": "If set, exit crawler and fail the scraper immediately if WARC size exceeds this value", + "min": 0 + }, + "disk_utilization": { + "type": "integer", + "required": false, + "title": "Disk Utilization", + "description": "Save state and exit if disk utilization exceeds this percentage value. Default (if not set) is 90%. Set to 0 to disable disk utilization check.", + "min": 0 + }, + "time_soft_limit": { + "type": "integer", + "required": false, + "title": "Time Soft Limit", + "description": "If set, save crawl state and stop crawl if WARC(s) creation takes longer than this value, in seconds. ZIM will still be created.", + "min": 0 + }, + "time_hard_limit": { + "type": "integer", + "required": false, + "title": "Time Hard Limit", + "description": "If set, exit crawler and fail the scraper immediately if WARC(s) creation takes longer than this value, in seconds", + "min": 0 + }, + "net_idle_wait": { + "type": "integer", + "required": false, + "title": "Net Idle Wait", + "description": "If set, wait for network idle after page load and after behaviors are done (in seconds). If -1 (default), determine based on scope." + }, + "origin_override": { + "type": "string", + "required": false, + "title": "Origin Override", + "description": "If set, will redirect requests from each origin in key to origin in the value, eg. https://host:port=http://alt-host:alt-port." + }, + "max_page_retries": { + "type": "integer", + "required": false, + "title": "Max Page Retries", + "description": "If set, number of times to retry a page that failed to load before page is considered to have failed. Default is 2.", + "min": 0 + }, + "fail_on_failed_seed": { + "type": "boolean", + "required": false, + "title": "Fail on failed seed", + "description": "Whether to display additional logs" + }, + "fail_on_invalid_status": { + "type": "boolean", + "required": false, + "title": "Fail on invalid status", + "description": "If set, will treat pages with 4xx or 5xx response as failures. When combined with --failOnFailedLimit or --failOnFailedSeed may result in crawl failing due to non-200 responses" + }, + "fail_on_failed_limit": { + "type": "integer", + "required": false, + "title": "Fail on failed - Limit", + "description": "If set, save state and exit if number of failed pages exceeds this value.", + "min": 0 + }, + "warcs": { + "type": "string", + "required": false, + "title": "WARC files", + "description": "Comma-separated list of WARC files to use as input." + }, + "verbose": { + "type": "boolean", + "required": false, + "title": "Verbose mode", + "description": "Whether to display additional logs" + }, + "keep": { + "type": "boolean", + "required": false, + "title": "Keep", + "description": "Should be True. Developer option: must be True if we want to keep the WARC files for artifacts archiving.", + "default": true + }, + "output": { + "type": "string", + "required": false, + "title": "Output folder", + "description": "Output folder for ZIM file(s). Leave it as `/output`", + "pattern": "^/output$" + }, + "admin_email": { + "type": "email", + "required": false, + "title": "Admin Email", + "description": "Admin Email for crawler: used in UserAgent so website admin can contact us", + "default": "contact+zimfarm@kiwix.org" + }, + "profile": { + "type": "string", + "required": false, + "title": "Browser profile", + "description": "Path or HTTP(S) URL to tar.gz file which contains the browser profile directory for Browsertrix crawler." + }, + "behaviors": { + "type": "string", + "required": false, + "title": "Behaviors", + "description": "Which background behaviors to enable on each page. Defaults to autoplay,autofetch,siteSpecific." + }, + "depth": { + "type": "integer", + "required": false, + "title": "Depth", + "description": "The depth of the crawl for all seeds. Default is -1 (infinite).", + "min": -1 + }, + "zim_lang": { + "type": "string", + "required": false, + "title": "ZIM Language", + "description": "Language metadata of ZIM (warc2zim --lang param). ISO-639-3 code. Retrieved from homepage if found, fallback to `eng`", + "alias": "zim-lang", + "customValidator": "language_code" + }, + "long_description": { + "type": "string", + "required": false, + "title": "Long description", + "description": "Optional long description for your ZIM", + "minLength": 1, + "maxLength": 4000, + "alias": "long-description" + }, + "custom_css": { + "type": "blob", + "kind": "css", + "required": false, + "title": "Custom CSS", + "description": "URL to a CSS file to inject into pages", + "alias": "custom-css" + }, + "charsets_to_try": { + "type": "string", + "required": false, + "title": "Charsets to try", + "description": "List of charsets to try decode content when charset is not found", + "alias": "charsets-to-try" + }, + "ignore_content_header_charsets": { + "type": "boolean", + "required": false, + "title": "Ignore Content Header Charsets", + "description": "Ignore the charsets specified in content headers - first bytes - typically because they are wrong.", + "alias": "ignore-content-header-charsets" + }, + "content_header_bytes_length": { + "type": "integer", + "required": false, + "title": "Content Header Bytes Length", + "description": "How many bytes to consider when searching for content charsets in header (default is 1024).", + "alias": "content-header-bytes-length", + "min": 0 + }, + "ignore_http_header_charsets": { + "type": "boolean", + "required": false, + "title": "Ignore HTTP Header Charsets", + "description": "Ignore the charsets specified in HTTP `Content-Type` headers, typically because they are wrong.", + "alias": "ignore-http-header-charsets" + }, + "encoding_aliases": { + "type": "string", + "required": false, + "title": "Encoding Aliases", + "description": "List of encoding/charset aliases to decode WARC content. Aliases are used when the encoding specified in upstream server exists in Python under a different name. This parameter is single string, multiple values are separated by a comma, like in alias1=encoding1,alias2=encoding2.", + "alias": "encoding-aliases" + }, + "custom_behaviors": { + "type": "string", + "required": false, + "title": "Custom Behaviors", + "description": "JS code for custom behaviors to customize crawler. Single string with individual JS files URL/path separated by a comma.", + "alias": "custom-behaviours" + }, + "zimit_progress_file": { + "type": "string", + "required": false, + "title": "Zimit Progress File", + "description": "Scraping progress file. Leave it as `/output/task_progress.json`", + "alias": "zimit-progress-file", + "pattern": "^/output/task_progress\\.json$" + }, + "replay_viewer_source": { + "type": "url", + "required": false, + "title": "Replay Viewer Source", + "description": "URL from which to load the ReplayWeb.page replay viewer from", + "alias": "replay-viewer-source" + }, + "zim_file": { + "type": "string", + "required": false, + "title": "ZIM filename", + "description": "ZIM file name (based on --name if not provided). Include {period} to insert date period dynamically", + "alias": "zim-file", + "pattern": "^([a-z0-9\\-\\.]+_)([a-z\\-]+_)([a-z0-9\\-\\.]+_)([a-z0-9\\-\\.]+_|)([\\d]{4}-[\\d]{2}|\\{period\\}).zim$", + "relaxedPattern": "^[A-Za-z0-9._-]+$" + }, + "name": { + "type": "string", + "required": true, + "title": "ZIM name", + "description": "Name of the ZIM.", + "alias": "name", + "pattern": "^([a-z0-9\\-\\.]+_)([a-z\\-]+_)([a-z0-9\\-\\.]+)$", + "relaxedPattern": "^[A-Za-z0-9._-]+$" + }, + "overwrite": { + "type": "boolean", + "required": false, + "title": "Overwrite", + "description": "Whether to overwrite existing ZIM file if it exists" + } + } +} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..e4e7696 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,225 @@ +[build-system] +requires = ["hatchling", "hatch-openzim"] +build-backend = "hatchling.build" + +[project] +name = "zimit" +requires-python = ">=3.13,<3.14" +description = "Make ZIM file from any website through crawling" +readme = "README.md" +dependencies = [ + "requests==2.32.3", + "inotify==0.2.10", + "tld==0.13", + "warc2zim @ git+https://github.com/openzim/warc2zim@main", +] +dynamic = ["authors", "classifiers", "keywords", "license", "version", "urls"] + +[tool.hatch.metadata.hooks.openzim-metadata] +kind = "scraper" + +[tool.hatch.metadata] +allow-direct-references = true # to be removed once we use a released warc2zim version + +[project.optional-dependencies] +scripts = [ + "invoke==2.2.0", +] +lint = [ + "black==25.1.0", + "ruff==0.9.4", +] +check = [ + "pyright==1.1.393", +] +test = [ + "pytest==8.3.4", + "coverage==7.6.10", +] +dev = [ + "pre-commit==4.1.0", + "debugpy==1.8.12", + "selenium==4.28.1", # used in daily tests, convenient for dev purpose (autocompletion) + "zimit[scripts]", + "zimit[lint]", + "zimit[test]", + "zimit[check]", +] + +[project.scripts] +zimit = "zimit:zimit.zimit" + +[tool.hatch.version] +path = "src/zimit/__about__.py" + +[tool.hatch.build] +exclude = [ + "/.github", +] + +[tool.hatch.build.targets.wheel] +packages = ["src/zimit"] + +[tool.hatch.envs.default] +features = ["dev"] + +[tool.hatch.envs.test] +features = ["scripts", "test"] + +[tool.hatch.envs.test.scripts] +run = "inv test --args '{args}'" +run-cov = "inv test-cov --args '{args}'" +report-cov = "inv report-cov" +coverage = "inv coverage --args '{args}'" +html = "inv coverage --html --args '{args}'" + +[tool.hatch.envs.lint] +template = "lint" +skip-install = false +features = ["scripts", "lint"] + +[tool.hatch.envs.lint.scripts] +black = "inv lint-black --args '{args}'" +ruff = "inv lint-ruff --args '{args}'" +all = "inv lintall --args '{args}'" +fix-black = "inv fix-black --args '{args}'" +fix-ruff = "inv fix-ruff --args '{args}'" +fixall = "inv fixall --args '{args}'" + +[tool.hatch.envs.check] +features = ["scripts", "check"] + +[tool.hatch.envs.check.scripts] +pyright = "inv check-pyright --args '{args}'" +all = "inv checkall --args '{args}'" + +[tool.black] +line-length = 88 +target-version = ['py313'] + +[tool.ruff] +target-version = "py313" +line-length = 88 +src = ["src"] + +[tool.ruff.lint] +select = [ + "A", # flake8-builtins + # "ANN", # flake8-annotations + "ARG", # flake8-unused-arguments + # "ASYNC", # flake8-async + "B", # flake8-bugbear + # "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + "C90", # mccabe + # "COM", # flake8-commas + # "D", # pydocstyle + # "DJ", # flake8-django + "DTZ", # flake8-datetimez + "E", # pycodestyle (default) + "EM", # flake8-errmsg + # "ERA", # eradicate + # "EXE", # flake8-executable + "F", # Pyflakes (default) + # "FA", # flake8-future-annotations + "FBT", # flake8-boolean-trap + # "FLY", # flynt + # "G", # flake8-logging-format + "I", # isort + "ICN", # flake8-import-conventions + # "INP", # flake8-no-pep420 + # "INT", # flake8-gettext + "ISC", # flake8-implicit-str-concat + "N", # pep8-naming + # "NPY", # NumPy-specific rules + # "PD", # pandas-vet + # "PGH", # pygrep-hooks + # "PIE", # flake8-pie + # "PL", # Pylint + "PLC", # Pylint: Convention + "PLE", # Pylint: Error + "PLR", # Pylint: Refactor + "PLW", # Pylint: Warning + # "PT", # flake8-pytest-style + # "PTH", # flake8-use-pathlib + # "PYI", # flake8-pyi + "Q", # flake8-quotes + # "RET", # flake8-return + # "RSE", # flake8-raise + "RUF", # Ruff-specific rules + "S", # flake8-bandit + # "SIM", # flake8-simplify + # "SLF", # flake8-self + "T10", # flake8-debugger + "T20", # flake8-print + # "TCH", # flake8-type-checking + # "TD", # flake8-todos + "TID", # flake8-tidy-imports + # "TRY", # tryceratops + "UP", # pyupgrade + "W", # pycodestyle + "YTT", # flake8-2020 +] +ignore = [ + # Allow non-abstract empty methods in abstract base classes + "B027", + # Remove flake8-errmsg since we consider they bloat the code and provide limited value + "EM", + # Allow boolean positional values in function calls, like `dict.get(... True)` + "FBT003", + # Ignore checks for possible passwords + "S105", "S106", "S107", + # Ignore warnings on subprocess.run / popen + "S603", + # Ignore complexity + "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915", +] +unfixable = [ + # Don't touch unused imports + "F401", +] + +[tool.ruff.lint.isort] +known-first-party = ["zimit"] + +[tool.ruff.lint.flake8-bugbear] +# add exceptions to B008 for fastapi. +extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"] + +[tool.ruff.lint.flake8-tidy-imports] +ban-relative-imports = "all" + +[tool.ruff.lint.per-file-ignores] +# Tests can use magic values, assertions, and relative imports +"tests**/**/*" = ["PLR2004", "S101", "TID252"] + +[tool.pytest.ini_options] +minversion = "7.3" +testpaths = ["tests"] +pythonpath = [".", "src"] + +[tool.coverage.paths] +zimit = ["src/zimit"] +tests = ["tests"] + +[tool.coverage.run] +source_pkgs = ["zimit"] +branch = true +parallel = true +omit = [ + "src/zimit/__about__.py", +] + +[tool.coverage.report] +exclude_lines = [ + "no cov", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", +] + +[tool.pyright] +include = ["src", "tests", "tasks.py"] +exclude = [".env/**", ".venv/**"] +extraPaths = ["src"] +pythonVersion = "3.13" +typeCheckingMode="basic" diff --git a/setup.py b/setup.py deleted file mode 100644 index 442f352..0000000 --- a/setup.py +++ /dev/null @@ -1,33 +0,0 @@ -import os -from setuptools import setup, find_packages - -here = os.path.abspath(os.path.dirname(__file__)) - -with open(os.path.join(here, 'README.rst')) as f: - README = f.read() - - -setup(name='zimit', - version=0.1, - description='zimit', - long_description=README, - classifiers=[ - "Programming Language :: Python", - "Framework :: Pylons", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Internet :: WWW/HTTP :: WSGI :: Application" - ], - keywords="web services", - author='', - author_email='', - url='', - packages=find_packages(), - include_package_data=True, - zip_safe=False, - install_requires=['cornice', 'waitress', 'rq', 'colander', - 'python-slugify', 'pyramid_mailer'], - entry_points="""\ - [paste.app_factory] - main=zimit:main - """, - paster_plugins=['pyramid']) diff --git a/src/zimit/__about__.py b/src/zimit/__about__.py new file mode 100644 index 0000000..281b1bb --- /dev/null +++ b/src/zimit/__about__.py @@ -0,0 +1 @@ +__version__ = "3.0.6-dev0" diff --git a/src/zimit/constants.py b/src/zimit/constants.py new file mode 100644 index 0000000..35baeb9 --- /dev/null +++ b/src/zimit/constants.py @@ -0,0 +1,11 @@ +import logging + +from zimscraperlib.logging import getLogger + +EXIT_CODE_WARC2ZIM_CHECK_FAILED = 2 +EXIT_CODE_CRAWLER_SIZE_LIMIT_HIT = 14 +EXIT_CODE_CRAWLER_TIME_LIMIT_HIT = 15 +NORMAL_WARC2ZIM_EXIT_CODE = 100 +REQUESTS_TIMEOUT = 10 + +logger = getLogger(name="zimit", level=logging.INFO) diff --git a/src/zimit/utils.py b/src/zimit/utils.py new file mode 100644 index 0000000..f2b78f4 --- /dev/null +++ b/src/zimit/utils.py @@ -0,0 +1,14 @@ +from pathlib import Path + +import requests + +from zimit.constants import REQUESTS_TIMEOUT + + +def download_file(url: str, fpath: Path): + """Download file from url to fpath with streaming""" + with requests.get(url, timeout=REQUESTS_TIMEOUT, stream=True) as resp: + resp.raise_for_status() + with open(fpath, "wb") as f: + for chunk in resp.iter_content(chunk_size=8192): + f.write(chunk) diff --git a/src/zimit/zimit.py b/src/zimit/zimit.py new file mode 100755 index 0000000..b205007 --- /dev/null +++ b/src/zimit/zimit.py @@ -0,0 +1,1261 @@ +""" +Main zimit run script +This script validates arguments with warc2zim, checks permissions +and then calls the Node based driver +""" + +import atexit +import json +import re +import shutil +import signal +import subprocess +import sys +import tarfile +import tempfile +import urllib.parse +from argparse import ArgumentParser +from multiprocessing import Process +from pathlib import Path + +import inotify +import inotify.adapters +from warc2zim.main import main as warc2zim +from zimscraperlib.uri import rebuild_uri + +from zimit.__about__ import __version__ +from zimit.constants import ( + EXIT_CODE_CRAWLER_SIZE_LIMIT_HIT, + EXIT_CODE_CRAWLER_TIME_LIMIT_HIT, + EXIT_CODE_WARC2ZIM_CHECK_FAILED, + NORMAL_WARC2ZIM_EXIT_CODE, + logger, +) +from zimit.utils import download_file + +temp_root_dir: Path | None = None + + +class ProgressFileWatcher: + def __init__( + self, crawl_stats_path: Path, warc2zim_stats_path, zimit_stats_path: Path + ): + self.crawl_stats_path = crawl_stats_path + self.warc2zim_stats_path = warc2zim_stats_path + self.zimit_stats_path = zimit_stats_path + + # touch them all so inotify is not unhappy on add_watch + self.crawl_stats_path.touch() + self.warc2zim_stats_path.touch() + self.process = None + + def stop(self): + if not self.process: + return + self.process.join(0.1) + self.process.terminate() + + def watch(self): + self.process = Process( + target=self.inotify_watcher, + args=( + str(self.crawl_stats_path), + str(self.warc2zim_stats_path), + str(self.zimit_stats_path), + ), + ) + self.process.daemon = True + self.process.start() + + def inotify_watcher(self, crawl_fpath: str, warc2zim_fpath: str, zimit_fpath: str): + ino = inotify.adapters.Inotify() + ino.add_watch(crawl_fpath, inotify.constants.IN_MODIFY) # pyright: ignore + ino.add_watch(warc2zim_fpath, inotify.constants.IN_MODIFY) # pyright: ignore + + def crawl_conv(data): + # we consider crawl to be 90% of the workload so total = craw_total * 90% + return { + "done": data["crawled"], + "total": int(data["total"] / 0.9), + } + + def warc2zim_conv(data): + # we consider warc2zim to be 10% of the workload so + # warc2zim_total = 10% and total = 90 + warc2zim_total * 10% + return { + "done": int( + data["total"] + * (0.9 + (float(data["written"]) / data["total"]) / 10) + ), + "total": data["total"], + } + + for _, _, fpath, _ in ino.event_gen(yield_nones=False): # pyright: ignore + func = {crawl_fpath: crawl_conv, warc2zim_fpath: warc2zim_conv}.get(fpath) + if not func: + continue + # open input and output separatly as to not clear output on error + with open(fpath) as ifh: + try: + out = func(json.load(ifh)) + except Exception: # nosec # noqa: S112 + # simply ignore progress update should an error arise + # might be malformed input for instance + continue + if not out: + continue + with open(zimit_fpath, "w") as ofh: + json.dump(out, ofh) + + +def cleanup(): + if not temp_root_dir: + logger.warning("Temporary root dir not already set, cannot clean this up") + return + logger.info("") + logger.info("----------") + logger.info(f"Cleanup, removing temp dir: {temp_root_dir}") + shutil.rmtree(temp_root_dir) + + +def cancel_cleanup(): + logger.info( + f"Temporary files have been kept in {temp_root_dir}, please clean them" + " up manually once you don't need them anymore" + ) + atexit.unregister(cleanup) + + +def run(raw_args): + parser = ArgumentParser( + description="Run a browser-based crawl on the specified URL and convert to ZIM" + ) + + parser.add_argument( + "--seeds", + help="The seed URL(s) to start crawling from. Multile seed URL must be " + "separated by a comma (usually not needed, these are just the crawl seeds). " + "First seed URL is used as ZIM homepage", + ) + + parser.add_argument("--title", help="WARC and ZIM title") + parser.add_argument("--description", help="WARC and ZIM description") + parser.add_argument("--long-description", help="ZIM long description metadata") + + parser.add_argument( + "--seedFile", + help="If set, read a list of seed urls, one per line. Can be a local file or " + "the HTTP(s) URL to an online file.", + ) + + parser.add_argument( + "-w", "--workers", type=int, help="Number of parallel workers. Default is 1." + ) + + parser.add_argument( + "--crawlId", + help="A user provided ID for this crawl or crawl configuration (can also be " + "set via CRAWL_ID env var, defaults to machine hostname)", + ) + + parser.add_argument( + "--waitUntil", + help="Puppeteer page.goto() condition to wait for before continuing. One of " + "load, domcontentloaded, networkidle0 or networkidle2, or a " + "comma-separated combination of those. Default is load,networkidle2", + ) + + parser.add_argument( + "--depth", + help="The depth of the crawl for all seeds. Default is -1 (infinite).", + type=int, + ) + + parser.add_argument( + "--extraHops", + help="Number of extra 'hops' to follow, beyond the current scope. " + "Default is 0.", + type=int, + ) + + parser.add_argument( + "--pageLimit", + help="Limit crawl to this number of pages. Default is 0 (no limit).", + type=int, + ) + + parser.add_argument( + "--maxPageLimit", + help="Maximum pages to crawl, overriding pageLimit if both are set. Default is " + "0 (no limit)", + type=int, + ) + + parser.add_argument( + "--pageLoadTimeout", + help="Timeout for each page to load (in seconds). Default is 90 secs.", + type=int, + ) + + parser.add_argument( + "--scopeType", + help="A predfined scope of the crawl. For more customization, " + "use 'custom' and set scopeIncludeRx/scopeExcludeRx regexes. Default is custom" + "if scopeIncludeRx is set, prefix otherwise.", + choices=["page", "page-spa", "prefix", "host", "domain", "any", "custom"], + ) + + parser.add_argument( + "--scopeIncludeRx", + help="Regex of page URLs that should be included in the crawl (defaults to " + "the immediate directory of URL)", + ) + + parser.add_argument( + "--scopeExcludeRx", + help="Regex of page URLs that should be excluded from the crawl", + ) + + parser.add_argument( + "--allowHashUrls", + help="Allow Hashtag URLs, useful for single-page-application crawling or " + "when different hashtags load dynamic content", + action="store_true", + ) + + parser.add_argument( + "--selectLinks", + help="One or more selectors for extracting links, in the format " + "[css selector]->[property to use],[css selector]->@[attribute to use]", + ) + + parser.add_argument( + "--clickSelector", + help="Selector for elements to click when using the autoclick behavior. Default" + " is 'a'", + ) + + parser.add_argument( + "--blockRules", + help="Additional rules for blocking certain URLs from being loaded, by URL " + "regex and optionally via text match in an iframe", + ) + + parser.add_argument( + "--blockMessage", + help="If specified, when a URL is blocked, a record with this error message is" + " added instead", + ) + + parser.add_argument( + "--blockAds", + help="If set, block advertisements from being loaded (based on Stephen Black's" + " blocklist). Note that some bad domains are also blocked by zimit" + " configuration even if this option is not set.", + ) + + parser.add_argument( + "--adBlockMessage", + help="If specified, when an ad is blocked, a record with this error message is" + " added instead", + ) + + parser.add_argument( + "--collection", + help="Collection name to crawl to (replay will be accessible " + "under this name in pywb preview). Default is crawl-@ts.", + ) + + parser.add_argument( + "--headless", + help="Run in headless mode, otherwise start xvfb", + action="store_true", + ) + + parser.add_argument( + "--driver", + help="Custom driver for the crawler, if any", + ) + + parser.add_argument( + "--generateCDX", + help="If set, generate index (CDXJ) for use with pywb after crawl is done", + action="store_true", + ) + + parser.add_argument( + "--combineWARC", + help="If set, combine the warcs", + action="store_true", + ) + + parser.add_argument( + "--rolloverSize", + help="If set, declare the rollover size. Default is 1000000000.", + type=int, + ) + + parser.add_argument( + "--generateWACZ", + help="If set, generate WACZ on disk", + action="store_true", + ) + + parser.add_argument( + "--logging", + help="Crawler logging configuration", + ) + + parser.add_argument( + "--logLevel", + help="Comma-separated list of log levels to include in logs", + ) + + parser.add_argument( + "--logContext", + help="Comma-separated list of contexts to include in logs", + choices=[ + "general", + "worker", + "recorder", + "recorderNetwork", + "writer", + "state", + "redis", + "storage", + "text", + "exclusion", + "screenshots", + "screencast", + "originOverride", + "healthcheck", + "browser", + "blocking", + "behavior", + "behaviorScript", + "jsError", + "fetch", + "pageStatus", + "memoryStatus", + "crawlStatus", + "links", + "sitemap", + "wacz", + "replay", + "proxy", + ], + ) + + parser.add_argument( + "--logExcludeContext", + help="Comma-separated list of contexts to NOT include in logs. Default is " + "recorderNetwork,jsError,screencast", + choices=[ + "general", + "worker", + "recorder", + "recorderNetwork", + "writer", + "state", + "redis", + "storage", + "text", + "exclusion", + "screenshots", + "screencast", + "originOverride", + "healthcheck", + "browser", + "blocking", + "behavior", + "behaviorScript", + "jsError", + "fetch", + "pageStatus", + "memoryStatus", + "crawlStatus", + "links", + "sitemap", + "wacz", + "replay", + "proxy", + ], + ) + + parser.add_argument( + "--text", + help="Extract initial (default) or final text to pages.jsonl or WARC resource" + " record(s)", + ) + + # cwd is manipulated directly by zimit, based on --output / --build, we do not want + # to expose this setting + + parser.add_argument( + "--mobileDevice", + help="Emulate mobile device by name from " + "https://github.com/puppeteer/puppeteer/blob/" + "main/packages/puppeteer-core/src/common/Device.ts", + ) + + parser.add_argument( + "--userAgent", + help="Override default user-agent with specified value ; --userAgentSuffix and " + "--adminEmail have no effect when this is set", + ) + + parser.add_argument( + "--userAgentSuffix", + help="Append suffix to existing browser user-agent " + "(ex: +MyCrawler, info@example.com)", + default="+Zimit", + ) + + parser.add_argument( + "--useSitemap", + help="If set, use the URL as sitemap to get additional URLs for the crawl " + "(usually /sitemap.xml)", + ) + + parser.add_argument( + "--sitemapFromDate", + help="If set, filter URLs from sitemaps to those greater than or equal to (>=)" + " provided ISO Date string (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS or partial date)", + ) + + parser.add_argument( + "--sitemapToDate", + help="If set, filter URLs from sitemaps to those less than or equal to (<=) " + "provided ISO Date string (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS or partial date)", + ) + + parser.add_argument( + "--statsFilename", + help="If set, output crawl stats as JSON to this file. Relative filename " + "resolves to output directory, see --output.", + ) + + parser.add_argument( + "--zimit-progress-file", + help="If set, output zimit stats as JSON to this file. Forces the creation of" + "crawler and warc2zim stats as well. If --statsFilename and/or " + "--warc2zim-progress-file are not set, default temporary files will be used. " + "Relative filename resolves to output directory, see --output.", + ) + + parser.add_argument( + "--warc2zim-progress-file", + help="If set, output warc2zim stats as JSON to this file. Relative filename " + "resolves to output directory, see --output.", + ) + + parser.add_argument( + "--behaviors", + help="Which background behaviors to enable on each page. Default is autoplay," + "autofetch,autoscroll,siteSpecific", + ) + + parser.add_argument( + "--behaviorTimeout", + help="If >0, timeout (in seconds) for in-page behavior will run on each page. " + "If 0, a behavior can run until finish. Default is 90.", + type=int, + ) + + parser.add_argument( + "--postLoadDelay", + help="If >0, amount of time to sleep (in seconds) after page has loaded, before" + " taking screenshots / getting text / running behaviors. Default is 0.", + type=int, + ) + + parser.add_argument( + "--pageExtraDelay", + help="If >0, amount of time to sleep (in seconds) after behaviors " + "before moving on to next page. Default is 0.", + type=int, + ) + + parser.add_argument( + "--dedupPolicy", + help="Deduplication policy. Default is skip", + choices=["skip", "revisit", "keep"], + ) + + parser.add_argument( + "--profile", + help="Path or HTTP(S) URL to tar.gz file which contains the browser profile " + "directory", + ) + + parser.add_argument( + "--screenshot", + help="Screenshot options for crawler. One of view, thumbnail, fullPage, " + "fullPageFinal or a comma-separated combination of those.", + ) + + parser.add_argument( + "--screencastPort", + help="If set to a non-zero value, starts an HTTP server with screencast " + "accessible on this port.", + type=int, + ) + + parser.add_argument( + "--screencastRedis", + help="If set, will use the state store redis pubsub for screencasting", + action="store_true", + ) + + parser.add_argument( + "--warcInfo", + help="Optional fields added to the warcinfo record in combined WARCs", + ) + + parser.add_argument( + "--saveState", + help="If the crawl state should be serialized to the crawls/ directory. " + "Defaults to 'partial', only saved when crawl is interrupted", + choices=["never", "partial", "always"], + ) + + parser.add_argument( + "--saveStateInterval", + help="If save state is set to 'always', also save state during the crawl at " + "this interval (in seconds). Default to 300.", + type=int, + ) + + parser.add_argument( + "--saveStateHistory", + help="Number of save states to keep during the duration of a crawl. " + "Default to 5.", + type=int, + ) + + size_group = parser.add_mutually_exclusive_group() + size_group.add_argument( + "--sizeSoftLimit", + help="If set, save crawl state and stop crawl if WARC size exceeds this value. " + "ZIM will still be created.", + type=int, + ) + size_group.add_argument( + "--sizeHardLimit", + help="If set, exit crawler and fail the scraper immediately if WARC size " + "exceeds this value", + type=int, + ) + + parser.add_argument( + "--diskUtilization", + help="Save state and exit if disk utilization exceeds this percentage value." + " Default (if not set) is 90%%. Set to 0 to disable disk utilization check.", + type=int, + default=90, + ) + + time_group = parser.add_mutually_exclusive_group() + time_group.add_argument( + "--timeSoftLimit", + help="If set, save crawl state and stop crawl if WARC WARC(s) creation takes " + "longer than this value, in seconds. ZIM will still be created.", + type=int, + ) + time_group.add_argument( + "--timeHardLimit", + help="If set, exit crawler and fail the scraper immediately if WARC(s) creation" + " takes longer than this value, in seconds", + type=int, + ) + + parser.add_argument( + "--healthCheckPort", + help="port to run healthcheck on", + type=int, + ) + + parser.add_argument( + "--overwrite", + help="overwrite current crawl data: if set, existing collection directory " + "will be deleted before crawl is started", + action="store_true", + ) + + parser.add_argument( + "--waitOnDone", + help="if set, wait for interrupt signal when finished instead of exiting", + action="store_true", + ) + + parser.add_argument( + "--restartsOnError", + help="if set, assume will be restarted if interrupted, don't run post-crawl " + "processes on interrupt", + action="store_true", + ) + + parser.add_argument( + "--netIdleWait", + help="If set, wait for network idle after page load and after behaviors are " + "done (in seconds). if -1 (default), determine based on scope.", + type=int, + ) + + parser.add_argument( + "--lang", + help="if set, sets the language used by the browser, should be ISO 639 " + "language[-country] code", + ) + + parser.add_argument( + "--originOverride", + help="if set, will redirect requests from each origin in key to origin in the " + "value, eg. --originOverride https://host:port=http://alt-host:alt-port", + ) + + parser.add_argument( + "--logErrorsToRedis", + help="If set, write error messages to redis", + action="store_true", + ) + + parser.add_argument( + "--writePagesToRedis", + help="If set, write page objects to redis", + action="store_true", + ) + + parser.add_argument( + "--maxPageRetries", + help="If set, number of times to retry a page that failed to load before page" + " is considered to have failed. Default is 2.", + type=int, + ) + + parser.add_argument( + "--failOnFailedSeed", + help="If set, crawler will fail with exit code 1 if any seed fails. When " + "combined with --failOnInvalidStatus, will result in crawl failing with exit " + "code 1 if any seed has a 4xx/5xx response", + action="store_true", + ) + + parser.add_argument( + "--failOnFailedLimit", + help="If set, save state and exit if number of failed pages exceeds this value", + action="store_true", + ) + + parser.add_argument( + "--failOnInvalidStatus", + help="If set, will treat pages with 4xx or 5xx response as failures. When " + "combined with --failOnFailedLimit or --failOnFailedSeed may result in crawl " + "failing due to non-200 responses", + action="store_true", + ) + + # customBehaviors not included because it has special handling + # debugAccessRedis not included due to custom redis engine in zimit + + parser.add_argument( + "--debugAccessBrowser", + help="if set, allow debugging browser on port 9222 via CDP", + action="store_true", + ) + + parser.add_argument( + "--warcPrefix", + help="prefix for WARC files generated, including WARCs added to WACZ", + ) + + parser.add_argument( + "--serviceWorker", + help="service worker handling: disabled, enabled or disabled-if-profile. " + "Default: disabled.", + ) + + parser.add_argument( + "--proxyServer", + help="if set, will use specified proxy server. Takes precedence over any env " + "var proxy settings", + ) + + parser.add_argument( + "--dryRun", + help="If true, no archive data is written to disk, only pages and logs (and " + "optionally saved state).", + action="store_true", + ) + + parser.add_argument( + "--qaSource", + help="Required for QA mode. Path to the source WACZ or multi WACZ file for QA", + ) + + parser.add_argument( + "--qaDebugImageDiff", + help="if specified, will write crawl.png, replay.png and diff.png for each " + "page where they're different", + action="store_true", + ) + + parser.add_argument( + "--sshProxyPrivateKeyFile", + help="path to SSH private key for SOCKS5 over SSH proxy connection", + ) + + parser.add_argument( + "--sshProxyKnownHostsFile", + help="path to SSH known hosts file for SOCKS5 over SSH proxy connection", + ) + + parser.add_argument( + "--keep", + help="In case of failure, WARC files and other temporary files (which are " + "stored as a subfolder of output directory) are always kept, otherwise " + "they are automatically deleted. Use this flag to always keep WARC files, " + "even in case of success.", + action="store_true", + ) + + parser.add_argument( + "--output", + help="Output directory for ZIM. Default to /output.", + default="/output", + ) + + parser.add_argument( + "--build", + help="Build directory for WARC files (if not set, output directory is used)", + ) + + parser.add_argument("--adminEmail", help="Admin Email for Zimit crawler") + + parser.add_argument( + "--custom-css", + help="[warc2zim] Custom CSS file URL/path to inject into all articles", + ) + + parser.add_argument( + "--config", + help="Path to YAML config file. If set, browsertrix-crawler will use this file" + "to configure the crawling behaviour if not set via argument.", + ) + + parser.add_argument( + "--version", + help="Display scraper version and exit", + action="version", + version=f"Zimit {__version__}", + ) + + parser.add_argument( + "--zim-lang", + help="Language metadata of ZIM " + "(warc2zim --lang param). ISO-639-3 code. " + "Retrieved from homepage if found, fallback to `eng`", + ) + + parser.add_argument( + "--custom-behaviors", + help="JS code for custom behaviors to customize crawler. Single string with " + "individual JS files URL/path separated by a comma", + ) + + parser.add_argument( + "--warcs", + help="Directly convert WARC archives to ZIM, by-passing the crawling phase. " + "This argument must contain the path or HTTP(S) URL to either warc.gz files or" + "to a tar or tar.gz containing the warc.gz files. Single value with individual " + "path/URLs separated by comma", + ) + + parser.add_argument( + "--acceptable-crawler-exit-codes", + help="Non-zero crawler exit codes to consider as acceptable to continue with " + " conversion of WARC to ZIM. Flag partialZim will be set in statsFilename (if " + " used). Single value with individual error codes separated by comma", + ) + + # by design, all unknown args are for warc2zim ; known one are either for crawler + # or shared + known_args, warc2zim_args = parser.parse_known_args(raw_args) + + # pass a scraper suffix to warc2zim so that both zimit and warc2zim versions are + # associated with the ZIM ; make it a CSV for easier parsing + warc2zim_args.append("--scraper-suffix") + warc2zim_args.append(f"zimit {__version__}") + + # pass url and output to warc2zim also + if known_args.output: + warc2zim_args.append("--output") + warc2zim_args.append(known_args.output) + + user_agent_suffix = known_args.userAgentSuffix + if known_args.adminEmail: + user_agent_suffix += f" {known_args.adminEmail}" + + # set temp dir to use for this crawl + global temp_root_dir # noqa: PLW0603 + if known_args.build: + # use build dir argument if passed + temp_root_dir = Path(known_args.build) + temp_root_dir.mkdir(parents=True, exist_ok=True) + else: + # make new randomized temp dir + temp_root_dir = Path(tempfile.mkdtemp(dir=known_args.output, prefix=".tmp")) + + seeds = [] + if known_args.seeds: + seeds += [get_cleaned_url(url) for url in known_args.seeds.split(",")] + if known_args.seedFile: + if re.match(r"^https?\://", known_args.seedFile): + with tempfile.NamedTemporaryFile( + dir=temp_root_dir, + prefix="seeds_", + suffix=".txt", + delete_on_close=True, + ) as filename: + seed_file = Path(filename.name) + download_file(known_args.seedFile, seed_file) + seeds += [ + get_cleaned_url(url) for url in seed_file.read_text().splitlines() + ] + else: + seeds += [ + get_cleaned_url(url) + for url in Path(known_args.seedFile).read_text().splitlines() + ] + warc2zim_args.append("--url") + warc2zim_args.append(seeds[0]) + + if known_args.custom_css: + warc2zim_args += ["--custom-css", known_args.custom_css] + + if known_args.title: + warc2zim_args.append("--title") + warc2zim_args.append(known_args.title) + + if known_args.description: + warc2zim_args.append("--description") + warc2zim_args.append(known_args.description) + + if known_args.long_description: + warc2zim_args.append("--long-description") + warc2zim_args.append(known_args.long_description) + + if known_args.zim_lang: + warc2zim_args.append("--lang") + warc2zim_args.append(known_args.zim_lang) + + if known_args.overwrite: + warc2zim_args.append("--overwrite") + + logger.info("----------") + logger.info("Testing warc2zim args") + logger.info("Running: warc2zim " + " ".join(warc2zim_args)) + res = warc2zim(warc2zim_args) + if res != NORMAL_WARC2ZIM_EXIT_CODE: + logger.info("Exiting, invalid warc2zim params") + return EXIT_CODE_WARC2ZIM_CHECK_FAILED + + # only trigger cleanup when the keep argument is passed without a custom build dir. + if not known_args.build and not known_args.keep: + atexit.register(cleanup) + + # copy / download custom behaviors to one single folder and configure crawler + if known_args.custom_behaviors: + behaviors_dir = temp_root_dir / "custom-behaviors" + behaviors_dir.mkdir() + for custom_behavior in [ + custom_behavior.strip() + for custom_behavior in known_args.custom_behaviors.split(",") + ]: + behaviors_file = tempfile.NamedTemporaryFile( + dir=behaviors_dir, + prefix="behavior_", + suffix=".js", + delete_on_close=False, + ) + if re.match(r"^https?\://", custom_behavior): + logger.info( + f"Downloading browser profile from {custom_behavior} " + f"to {behaviors_file.name}" + ) + download_file(custom_behavior, Path(behaviors_file.name)) + else: + logger.info( + f"Copying browser profile from {custom_behavior} " + f"to {behaviors_file.name}" + ) + shutil.copy(custom_behavior, behaviors_file.name) + known_args.customBehaviors = str(behaviors_dir) + else: + known_args.customBehaviors = None + + crawler_args = get_crawler_cmd_line(known_args) + for seed in seeds: + crawler_args.append("--seeds") + crawler_args.append(seed) + + crawler_args.append("--userAgentSuffix") + crawler_args.append(user_agent_suffix) + + crawler_args.append("--cwd") + crawler_args.append(str(temp_root_dir)) + + output_dir = Path(known_args.output) + warc2zim_stats_file = ( + Path(known_args.warc2zim_progress_file) + if known_args.warc2zim_progress_file + else temp_root_dir / "warc2zim.json" + ) + if not warc2zim_stats_file.is_absolute(): + warc2zim_stats_file = output_dir / warc2zim_stats_file + warc2zim_stats_file.parent.mkdir(parents=True, exist_ok=True) + warc2zim_stats_file.unlink(missing_ok=True) + + crawler_stats_file = ( + Path(known_args.statsFilename) + if known_args.statsFilename + else temp_root_dir / "crawl.json" + ) + if not crawler_stats_file.is_absolute(): + crawler_stats_file = output_dir / crawler_stats_file + crawler_stats_file.parent.mkdir(parents=True, exist_ok=True) + crawler_stats_file.unlink(missing_ok=True) + + zimit_stats_file = ( + Path(known_args.zimit_progress_file) + if known_args.zimit_progress_file + else temp_root_dir / "stats.json" + ) + if not zimit_stats_file.is_absolute(): + zimit_stats_file = output_dir / zimit_stats_file + zimit_stats_file.parent.mkdir(parents=True, exist_ok=True) + zimit_stats_file.unlink(missing_ok=True) + + if known_args.zimit_progress_file: + # setup inotify crawler progress watcher + watcher = ProgressFileWatcher( + zimit_stats_path=zimit_stats_file, + crawl_stats_path=crawler_stats_file, + warc2zim_stats_path=warc2zim_stats_file, + ) + logger.info( + f"Writing zimit progress to {watcher.zimit_stats_path}, crawler progress to" + f" {watcher.crawl_stats_path} and warc2zim progress to " + f"{watcher.warc2zim_stats_path}" + ) + # update crawler command + crawler_args.append("--statsFilename") + crawler_args.append(str(crawler_stats_file)) + # update warc2zim command + warc2zim_args.append("-v") + warc2zim_args.append("--progress-file") + warc2zim_args.append(str(warc2zim_stats_file)) + watcher.watch() + else: + if known_args.statsFilename: + logger.info(f"Writing crawler progress to {crawler_stats_file}") + crawler_args.append("--statsFilename") + crawler_args.append(str(crawler_stats_file)) + if known_args.warc2zim_progress_file: + logger.info(f"Writing warc2zim progress to {warc2zim_stats_file}") + warc2zim_args.append("-v") + warc2zim_args.append("--progress-file") + warc2zim_args.append(str(warc2zim_stats_file)) + + cmd_line = " ".join(crawler_args) + + logger.info("") + logger.info("----------") + logger.info( + f"Output to tempdir: {temp_root_dir} - " + f"{'will keep' if known_args.keep else 'will delete'}" + ) + + partial_zim = False + + # if warc files are passed, do not run browsertrix crawler but fetch the files if + # they are provided as an HTTP URL + extract the archive if it is a tar.gz + warc_files: list[Path] = [] + if known_args.warcs: + for warc_location in [ + warc_location.strip() for warc_location in known_args.warcs.split(",") + ]: + suffix = "".join(Path(urllib.parse.urlparse(warc_location).path).suffixes) + if suffix not in {".tar", ".tar.gz", ".warc", ".warc.gz"}: + raise Exception(f"Unsupported file at {warc_location}") + + filename = tempfile.NamedTemporaryFile( + dir=temp_root_dir, + prefix="warc_", + suffix=suffix, + delete_on_close=False, + ) + + if not re.match(r"^https?\://", warc_location): + # warc_location is not a URL, so it is a path, simply add it to the list + if not Path(warc_location).exists(): + raise Exception(f"Impossible to find file at {warc_location}") + + # if it is a plain warc or warc.gz, simply add it to the list + if suffix in {".warc", ".warc.gz"}: + warc_files.append(Path(warc_location)) + continue + + # otherwise extract tar.gz but do not delete it afterwards + extract_path = temp_root_dir / f"{filename.name}_files" + logger.info( + f"Extracting WARC(s) from {warc_location} to {extract_path}" + ) + with tarfile.open(warc_location, "r") as fh: + # Extract all the contents to the specified directory + fh.extractall(path=extract_path, filter="data") + warc_files.append(Path(extract_path)) + continue + + # warc_location is a URL, let's download it to a temp name to avoid name + # collisions + warc_file = Path(filename.name) + logger.info(f"Downloading WARC(s) from {warc_location} to {warc_file}") + download_file(warc_location, warc_file) + + # if it is a plain warc or warc.gz, simply add it to the list + if suffix in {".warc", ".warc.gz"}: + warc_files.append(warc_file) + continue + + # otherwise extract tar.gz and delete it afterwards + extract_path = temp_root_dir / f"{filename.name}_files" + logger.info(f"Extracting WARC(s) from {warc_file} to {extract_path}") + with tarfile.open(warc_file, "r") as fh: + # Extract all the contents to the specified directory + fh.extractall(path=extract_path, filter="data") + logger.info(f"Deleting archive at {warc_file}") + warc_file.unlink() + warc_files.append(Path(extract_path)) + + else: + logger.info(f"Running browsertrix-crawler crawl: {cmd_line}") + crawl = subprocess.run(crawler_args, check=False) + if ( + crawl.returncode == EXIT_CODE_CRAWLER_SIZE_LIMIT_HIT + and known_args.sizeSoftLimit + ): + logger.info( + "Crawl size soft limit hit. Continuing with warc2zim conversion." + ) + if known_args.zimit_progress_file: + partial_zim = True + elif ( + crawl.returncode == EXIT_CODE_CRAWLER_TIME_LIMIT_HIT + and known_args.timeSoftLimit + ): + logger.info( + "Crawl time soft limit hit. Continuing with warc2zim conversion." + ) + if known_args.zimit_progress_file: + partial_zim = True + elif crawl.returncode != 0: + logger.error( + f"Crawl returned an error: {crawl.returncode}, scraper exiting" + ) + cancel_cleanup() + return crawl.returncode + + if known_args.collection: + warc_files = [ + temp_root_dir.joinpath(f"collections/{known_args.collection}/archive/") + ] + + else: + warc_dirs = sorted( + temp_root_dir.rglob("collections/crawl-*/archive/"), + key=lambda path: path.lstat().st_mtime, + ) + if len(warc_dirs) == 0: + raise RuntimeError( + "Failed to find directory where WARC files have been created" + ) + elif len(warc_dirs) > 1: + logger.info( + "Found many WARC files directories, combining pages from all " + "of them" + ) + for directory in warc_dirs: + logger.info(f"- {directory}") + warc_files = warc_dirs + + logger.info("") + logger.info("----------") + logger.info( + f"Processing WARC files in/at " + f"{' '.join(str(warc_file) for warc_file in warc_files)}" + ) + warc2zim_args.extend(str(warc_file) for warc_file in warc_files) + + logger.info(f"Calling warc2zim with these args: {warc2zim_args}") + + warc2zim_exit_code = warc2zim(warc2zim_args) + + if known_args.zimit_progress_file: + stats_content = json.loads(zimit_stats_file.read_bytes()) + stats_content["partialZim"] = partial_zim + zimit_stats_file.write_text(json.dumps(stats_content)) + + # also call cancel_cleanup when --keep, even if it is not supposed to be registered, + # so that we will display temporary files location just like in other situations + if warc2zim_exit_code or known_args.keep: + cancel_cleanup() + + return warc2zim_exit_code + + +def get_cleaned_url(url: str): + parsed_url = urllib.parse.urlparse(url) + + # remove explicit port in URI for default-for-scheme as browsers does it + if parsed_url.scheme == "https" and parsed_url.port == 443: # noqa: PLR2004 + parsed_url = rebuild_uri(parsed_url, port="") + if parsed_url.scheme == "http" and parsed_url.port == 80: # noqa: PLR2004 + parsed_url = rebuild_uri(parsed_url, port="") + + return parsed_url.geturl() + + +def get_crawler_cmd_line(args): + """Build the command line for Browsertrix crawler""" + node_cmd = ["crawl"] + for arg in [ + "title", + "description", + "workers", + "crawlId", + "waitUntil", + "depth", + "extraHops", + "pageLimit", + "maxPageLimit", + "pageLoadTimeout", + "scopeType", + "scopeIncludeRx", + "scopeExcludeRx", + "collection", + "allowHashUrls", + "selectLinks", + "clickSelector", + "blockRules", + "blockMessage", + "blockAds", + "adBlockMessage", + "collection", + "headless", + "driver", + "generateCDX", + "combineWARC", + "rolloverSize", + "generateWACZ", + "logging", + "logLevel", + "logContext", + "logExcludeContext", + "text", + "mobileDevice", + "userAgent", + # userAgentSuffix (manipulated), + "useSitemap", + "sitemapFromDate", + "sitemapToDate", + # statsFilename (manipulated), + "behaviors", + "behaviorTimeout", + "postLoadDelay", + "pageExtraDelay", + "dedupPolicy", + "profile", + "screenshot", + "screencastPort", + "screencastRedis", + "warcInfo", + "saveState", + "saveStateInterval", + "saveStateHistory", + "sizeSoftLimit", + "sizeHardLimit", + "diskUtilization", + "timeSoftLimit", + "timeHardLimit", + "healthCheckPort", + "overwrite", + "waitOnDone", + "restartsOnError", + "netIdleWait", + "lang", + "originOverride", + "logErrorsToRedis", + "writePagesToRedis", + "maxPageRetries", + "failOnFailedSeed", + "failOnFailedLimit", + "failOnInvalidStatus", + "debugAccessBrowser", + "warcPrefix", + "serviceWorker", + "proxyServer", + "dryRun", + "qaSource", + "qaDebugImageDiff", + "sshProxyPrivateKeyFile", + "sshProxyKnownHostsFile", + "customBehaviors", + "config", + ]: + value = getattr(args, arg) + if arg == "userAgent": + # - strip leading whitespace which are not allowed on some websites + # - strip trailing whitespace which are either not allowed if no suffix is + # used, or duplicate with the automatically added one if a suffix is there + # - value is None when userAgent is not passed + if value: + value = value.strip() + if not value: + # ignore empty userAgent arg and keep crawler default value if empty + continue + if value is None or (isinstance(value, bool) and value is False): + continue + node_cmd.append( + "--" + + ( + "sizeLimit" + if arg in ["sizeSoftLimit", "sizeHardLimit"] + else "timeLimit" if arg in ["timeSoftLimit", "timeHardLimit"] else arg + ) + ) + if not isinstance(value, bool): + node_cmd.append(str(value)) + + return node_cmd + + +def sigint_handler(*args): # noqa: ARG001 + logger.info("") + logger.info("") + logger.info("SIGINT/SIGTERM received, stopping zimit") + logger.info("") + logger.info("") + sys.exit(3) + + +def zimit(): + sys.exit(run(sys.argv[1:])) + + +signal.signal(signal.SIGINT, sigint_handler) +signal.signal(signal.SIGTERM, sigint_handler) + + +if __name__ == "__main__": + zimit() diff --git a/tasks.py b/tasks.py new file mode 100644 index 0000000..a95c71a --- /dev/null +++ b/tasks.py @@ -0,0 +1,109 @@ +# pyright: strict, reportUntypedFunctionDecorator=false +import os + +from invoke.context import Context +from invoke.tasks import task # pyright: ignore [reportUnknownVariableType] + +use_pty = not os.getenv("CI", "") + + +@task(optional=["args"], help={"args": "pytest additional arguments"}) +def test(ctx: Context, args: str = ""): + """run tests (without coverage)""" + ctx.run(f"pytest {args}", pty=use_pty) + + +@task(optional=["args"], help={"args": "pytest additional arguments"}) +def test_cov(ctx: Context, args: str = ""): + """run test vith coverage""" + ctx.run(f"coverage run -m pytest {args}", pty=use_pty) + + +@task(optional=["html"], help={"html": "flag to export html report"}) +def report_cov(ctx: Context, *, html: bool = False): + """report coverage""" + ctx.run("coverage combine", warn=True, pty=use_pty) + ctx.run("coverage report --show-missing", pty=use_pty) + if html: + ctx.run("coverage html", pty=use_pty) + + +@task( + optional=["args", "html"], + help={ + "args": "pytest additional arguments", + "html": "flag to export html report", + }, +) +def coverage(ctx: Context, args: str = "", *, html: bool = False): + """run tests and report coverage""" + test_cov(ctx, args=args) + report_cov(ctx, html=html) + + +@task(optional=["args"], help={"args": "black additional arguments"}) +def lint_black(ctx: Context, args: str = "."): + args = args or "." # needed for hatch script + ctx.run("black --version", pty=use_pty) + ctx.run(f"black --check --diff {args}", pty=use_pty) + + +@task(optional=["args"], help={"args": "ruff additional arguments"}) +def lint_ruff(ctx: Context, args: str = "."): + args = args or "." # needed for hatch script + ctx.run("ruff --version", pty=use_pty) + ctx.run(f"ruff check {args}", pty=use_pty) + + +@task( + optional=["args"], + help={ + "args": "linting tools (black, ruff) additional arguments, typically a path", + }, +) +def lintall(ctx: Context, args: str = "."): + """Check linting""" + args = args or "." # needed for hatch script + lint_black(ctx, args) + lint_ruff(ctx, args) + + +@task(optional=["args"], help={"args": "check tools (pyright) additional arguments"}) +def check_pyright(ctx: Context, args: str = ""): + """check static types with pyright""" + ctx.run("pyright --version") + ctx.run(f"pyright {args}", pty=use_pty) + + +@task(optional=["args"], help={"args": "check tools (pyright) additional arguments"}) +def checkall(ctx: Context, args: str = ""): + """check static types""" + check_pyright(ctx, args) + + +@task(optional=["args"], help={"args": "black additional arguments"}) +def fix_black(ctx: Context, args: str = "."): + """fix black formatting""" + args = args or "." # needed for hatch script + ctx.run(f"black {args}", pty=use_pty) + + +@task(optional=["args"], help={"args": "ruff additional arguments"}) +def fix_ruff(ctx: Context, args: str = "."): + """fix all ruff rules""" + args = args or "." # needed for hatch script + ctx.run(f"ruff check --fix {args}", pty=use_pty) + + +@task( + optional=["args"], + help={ + "args": "linting tools (black, ruff) additional arguments, typically a path", + }, +) +def fixall(ctx: Context, args: str = "."): + """Fix everything automatically""" + args = args or "." # needed for hatch script + fix_black(ctx, args) + fix_ruff(ctx, args) + lintall(ctx, args) diff --git a/tests-daily/Dockerfile b/tests-daily/Dockerfile new file mode 100644 index 0000000..22d45ef --- /dev/null +++ b/tests-daily/Dockerfile @@ -0,0 +1,75 @@ +# Let's extract kiwix-tools as usual on alpine temporary build container +FROM alpine:3.21 as kiwix-serve +LABEL org.opencontainers.image.source https://github.com/openzim/kiwix-tools + +# TARGETPLATFORM is injected by docker build +ARG TARGETPLATFORM +ARG KIWIX_TOOLS_VERSION + +RUN set -e && \ + # default (no KIWIX_TOOLS_VERSION set) to today's nightly + if [ -z "$KIWIX_TOOLS_VERSION" ] ; then KIWIX_TOOLS_VERSION=$(date +"%Y-%m-%d") ; fi && \ + apk --no-cache add dumb-init curl && \ + echo "TARGETPLATFORM: $TARGETPLATFORM" && \ + if [ "$TARGETPLATFORM" = "linux/386" ]; then ARCH="i586"; \ + # linux/arm64/v8 points to linux/arm64 + elif [ "$TARGETPLATFORM" = "linux/arm64/v8" \ + -o "$TARGETPLATFORM" = "linux/arm64" ]; then ARCH="aarch64"; \ + # linux/arm translates to linux/arm/v7 + elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then ARCH="armv8"; \ + elif [ "$TARGETPLATFORM" = "linux/arm/v6" ]; then ARCH="armv6"; \ + elif [ "$TARGETPLATFORM" = "linux/amd64/v3" \ + -o "$TARGETPLATFORM" = "linux/amd64/v2" \ + -o "$TARGETPLATFORM" = "linux/amd64" ]; then ARCH="x86_64"; \ + # we dont suppot any other arch so let it fail + else ARCH="unknown"; fi && \ + # download requested kiwix-tools version + url="http://mirror.download.kiwix.org/nightly/$KIWIX_TOOLS_VERSION/kiwix-tools_linux-$ARCH-$KIWIX_TOOLS_VERSION.tar.gz" && \ + echo "URL: $url" && \ + mkdir /kiwix-serve && \ + curl -k -L $url | tar -xz -C /kiwix-serve --strip-components 1 + +# Build real "workload" container +FROM python:3.13-slim-bookworm + +# Add kiwix-serve +COPY --from=kiwix-serve /kiwix-serve /usr/local/bin + +# Update apt + install dependencies + install Google Chrome dependencies + clean-up apt lists +RUN apt-get update -y && \ + apt-get install -qqy wget xvfb unzip jq && \ + apt-get install -qqy libxss1 libappindicator1 libgconf-2-4 \ + fonts-liberation libasound2 libnspr4 libnss3 libx11-xcb1 libxtst6 lsb-release xdg-utils \ + libgbm1 libnss3 libatk-bridge2.0-0 libgtk-3-0 libx11-xcb1 libxcb-dri3-0 && \ + rm -rf /var/lib/apt/lists/* + +# Fetch the latest version numbers and URLs for Chrome and ChromeDriver +RUN wget -q -O /tmp/versions.json https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json + +# Install chrome +RUN CHROME_URL=$(jq -r '.channels.Stable.downloads.chrome[] | select(.platform=="linux64") | .url' /tmp/versions.json) && \ + wget -q --continue -O /tmp/chrome-linux64.zip $CHROME_URL && \ + unzip /tmp/chrome-linux64.zip -d /opt/chrome + +RUN chmod +x /opt/chrome/chrome-linux64/chrome + +# Install chromedriver +RUN CHROMEDRIVER_URL=$(jq -r '.channels.Stable.downloads.chromedriver[] | select(.platform=="linux64") | .url' /tmp/versions.json) && \ + wget -q --continue -O /tmp/chromedriver-linux64.zip $CHROMEDRIVER_URL && \ + unzip /tmp/chromedriver-linux64.zip -d /opt/chromedriver && \ + chmod +x /opt/chromedriver/chromedriver-linux64/chromedriver + +# Set up Chromedriver Environment variables +ENV CHROMEDRIVER_DIR /opt/chromedriver +ENV PATH $CHROMEDRIVER_DIR:$PATH + +# Clean up +RUN rm /tmp/chrome-linux64.zip /tmp/chromedriver-linux64.zip /tmp/versions.json + +# Update pip, install selenium, create work directory +RUN \ + python -m pip install --no-cache-dir -U \ + pip \ + selenium==4.28.1 \ + pytest==8.3.4 \ +&& mkdir -p /work diff --git a/tests-daily/daily.py b/tests-daily/daily.py new file mode 100644 index 0000000..85172ec --- /dev/null +++ b/tests-daily/daily.py @@ -0,0 +1,128 @@ +import logging +import os +import subprocess +from time import sleep + +import pytest +from selenium import webdriver +from selenium.webdriver.chrome.options import Options +from selenium.webdriver.chrome.service import Service as ChromeService +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions +from selenium.webdriver.support.ui import WebDriverWait + +KIWIX_SERVE_START_SLEEP = 1 + +ZIM_NAME = "tests_eng_test-website" +YOUTUBE_VIDEO_PATH = "youtube.fuzzy.replayweb.page/embed/g5skcrNXdDM" + +SKIP_YOUTUBE_TEST = os.getenv("SKIP_YOUTUBE_TEST", "False").lower() == "true" + +CHECK_VIDEO_IS_PLAYING_AFTER_SECS = 30 + +logger = logging.getLogger(__name__) + + +@pytest.fixture(scope="module") +def chrome_driver(): + """Start chrome and setup chrome driver / selenium""" + + logger.info("Starting Chrome") + chrome_options = Options() + chrome_options.add_argument("--headless") + chrome_options.add_argument("--no-sandbox") + # Other options of interest: + # --disable-dev-shm-usage (not needed anymore with recent chrome versions) + # --disable-gpu (important for some versions of Chrome) + # --remote-debugging-port=9222 (should you need to remote debug) + + # Set path to Chrome binary + chrome_options.binary_location = "/opt/chrome/chrome-linux64/chrome" + + # Set path to ChromeDriver + chrome_service = ChromeService( + executable_path="/opt/chromedriver/chromedriver-linux64/chromedriver" + ) + + # Set up driver + driver = webdriver.Chrome(service=chrome_service, options=chrome_options) + + yield driver + + # Cleanup + logger.info("Quitting Chrome") + driver.quit() + + +@pytest.fixture(scope="module") +def kiwix_serve(): + """Start kiwix-serve with given ZIM""" + + logger.info("Starting kiwix-serve") + process = subprocess.Popen( + [ + "/usr/bin/env", + "/usr/local/bin/kiwix-serve", + f"/output/{ZIM_NAME}.zim", + ] + ) + + logger.info( + f"Waiting {KIWIX_SERVE_START_SLEEP} secs to be 'sure' that kiwix-serve is ready" + ) + sleep(KIWIX_SERVE_START_SLEEP) + + if process.poll() is not None: + raise Exception("kiwix-serve has terminated too early") + + yield process + + # Cleanup + logger.info("Quitting kiwix-serve") + process.terminate() + + +@pytest.mark.skipif(SKIP_YOUTUBE_TEST, reason="Youtube test disabled by environment") +def test_youtube_video(chrome_driver, kiwix_serve): # noqa: ARG001 + """Test that youtube video loads, and still plays after a while""" + + chrome_driver.get(f"http://localhost:80/content/{ZIM_NAME}/{YOUTUBE_VIDEO_PATH}") + + if chrome_driver.title == "Content not found": + raise Exception("Wrong URL, kiwix-serve said that content is not found") + + button = WebDriverWait(chrome_driver, 1).until( + expected_conditions.presence_of_element_located( + (By.XPATH, "//button[@title='Play']") + ) + ) + + logger.info("Play button found in page") + + button.click() + + video = WebDriverWait(chrome_driver, 1).until( + expected_conditions.presence_of_element_located((By.TAG_NAME, "video")) + ) + + logger.info("Video found in page") + + # arguments[0] is the video tag passed to execute_script + if not chrome_driver.execute_script("return arguments[0].paused === false", video): + raise Exception("Video is not playing, failed to start probably") + + logger.info("Video is playing") + + logger.info( + f"Waiting {CHECK_VIDEO_IS_PLAYING_AFTER_SECS} secs to check video is still " + "playing" + ) + sleep(CHECK_VIDEO_IS_PLAYING_AFTER_SECS) + + # arguments[0] is the video tag passed to execute_script + if not chrome_driver.execute_script("return arguments[0].paused === false", video): + raise Exception( + "Video is not playing anymore after " + f"{CHECK_VIDEO_IS_PLAYING_AFTER_SECS} secs" + ) + logger.info("Video is still playing") diff --git a/tests-integration/README.md b/tests-integration/README.md new file mode 100644 index 0000000..94da094 --- /dev/null +++ b/tests-integration/README.md @@ -0,0 +1 @@ +These are integration tests, meant to be ran inside the CI (because we need to first perform a zimit run on a given website and then check its output) diff --git a/tests-integration/integration.py b/tests-integration/integration.py new file mode 100644 index 0000000..7e79f52 --- /dev/null +++ b/tests-integration/integration.py @@ -0,0 +1,145 @@ +import glob +import json +import os +from pathlib import Path + +import pytest +from warcio import ArchiveIterator +from zimscraperlib.zim import Archive + + +@pytest.mark.parametrize( + "filename", + [ + pytest.param("/output/tests_en_onepage.zim", id="onepage"), + pytest.param("/output/tests_en_sizesoftlimit.zim", id="sizesoftlimit"), + pytest.param("/output/tests_en_timesoftlimit.zim", id="timesoftlimit"), + ], +) +def test_zim_created(filename): + """Ensure ZIM file exists""" + assert os.path.isfile(filename) + + +@pytest.mark.parametrize( + "filename", + [ + pytest.param("/output/tests_en_sizehardlimit.zim", id="sizehardlimit"), + pytest.param("/output/tests_en_timehardlimit.zim", id="timehardlimit"), + ], +) +def test_zim_not_created(filename): + """Ensure ZIM file does not exists""" + assert not os.path.exists(filename) + + +def test_zim_main_page(): + """Main page specified, http://website.test.openzim.org/http-return-codes.html, + was a redirect to https + Ensure main page is the redirected page""" + + main_entry = Archive(Path("/output/tests_en_onepage.zim")).main_entry + assert main_entry.is_redirect + assert ( + main_entry.get_redirect_entry().path + == "website.test.openzim.org/http-return-codes.html" + ) + + +def test_zim_scraper(): + """Check content of scraper metadata""" + + zim_fh = Archive(Path("/output/tests_en_onepage.zim")) + scraper = zim_fh.get_text_metadata("Scraper") + assert "zimit " in scraper + assert "warc2zim " in scraper + assert "Browsertrix-Crawler " in scraper + + +def test_files_list(): + """Check that expected files are present in the ZIM at proper path""" + zim_fh = Archive(Path("/output/tests_en_onepage.zim")) + for expected_entry in [ + "_zim_static/__wb_module_decl.js", + "_zim_static/wombat.js", + "_zim_static/wombatSetup.js", + "website.test.openzim.org/http-return-codes.html", + "website.test.openzim.org/200-response", + "website.test.openzim.org/201-response", + "website.test.openzim.org/202-response", + "website.test.openzim.org/301-external-redirect-ok", + "website.test.openzim.org/301-internal-redirect-ok", + "website.test.openzim.org/302-external-redirect-ok", + "website.test.openzim.org/302-internal-redirect-ok", + "website.test.openzim.org/307-external-redirect-ok", + "website.test.openzim.org/307-internal-redirect-ok", + "website.test.openzim.org/308-external-redirect-ok", + "website.test.openzim.org/308-internal-redirect-ok", + "website.test.openzim.org/http-return-codes.html", + "website.test.openzim.org/icons/favicon.ico", + "website.test.openzim.org/icons/site.webmanifest", + "website.test.openzim.org/internal_redirect_target.html", + "www.example.com/", + ]: + assert zim_fh.get_content(expected_entry) + + +def test_user_agent(): + """Test that mobile user agent was used + + Check is done in WARC request records with custom Zimit and email suffix + """ + + found = False + for warc in glob.glob("/output/.tmp*/collections/crawl-*/archive/*.warc.gz"): + with open(warc, "rb") as fh: + for record in ArchiveIterator(fh): + if record.rec_type == "request": + print(record.http_headers) # noqa: T201 + ua = record.http_headers.get_header("User-Agent") + if ua: + assert "Mozilla" in ua + assert ua.endswith(" +Zimit test@example.com") + found = True + + # should find at least one + assert found + + +def test_stats_output_standard(): + assert json.loads(Path("/output/crawl.json").read_bytes()) == { + "crawled": 17, + "pending": 0, + "pendingPages": [], + "total": 35, + "failed": 18, + "limit": {"max": 0, "hit": False}, + } + + assert json.loads(Path("/output/warc2zim.json").read_bytes()) == { + "written": 8, + "total": 8, + } + + assert json.loads(Path("/output/stats.json").read_bytes()) == { + "done": 8, + "total": 8, + "partialZim": False, + } + + +@pytest.mark.parametrize( + "filename", + [ + pytest.param("/output/stats_sizesoftlimit.json", id="sizesoftlimit"), + pytest.param("/output/stats_timesoftlimit.json", id="timesoftlimit"), + ], +) +def test_stats_output_softlimit(filename): + file = Path(filename) + assert file.exists + content = json.loads(file.read_bytes()) + assert "done" in content + assert "total" in content + assert "partialZim" in content + assert content["partialZim"] diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..d51650d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,14 @@ +import pytest + +from zimit import zimit as app + +""" + cleanup disabled because atexit hooks run at the very end of the Python process + shutdown. By the time cleanup() is called, the logging module has already closed its + file streams. +""" + + +@pytest.fixture(autouse=True) +def disable_zimit_cleanup(monkeypatch): + monkeypatch.setattr(app, "cleanup", lambda: None) diff --git a/tests/data/example-response.warc b/tests/data/example-response.warc new file mode 100644 index 0000000..143b947 Binary files /dev/null and b/tests/data/example-response.warc differ diff --git a/tests/test_dummy.py b/tests/test_dummy.py new file mode 100644 index 0000000..54af094 --- /dev/null +++ b/tests/test_dummy.py @@ -0,0 +1,6 @@ +from zimit.zimit import NORMAL_WARC2ZIM_EXIT_CODE + + +# dummy test, just to have coverage report done +def test_something_exists(): + assert NORMAL_WARC2ZIM_EXIT_CODE diff --git a/tests/test_overwrite.py b/tests/test_overwrite.py new file mode 100644 index 0000000..e41baca --- /dev/null +++ b/tests/test_overwrite.py @@ -0,0 +1,83 @@ +import pathlib + +import pytest + +from zimit.zimit import run + +TEST_DATA_DIR = pathlib.Path(__file__).parent / "data" + + +def test_overwrite_flag_behaviour(tmp_path): + zim_output = "overwrite-test.zim" + output_path = tmp_path / zim_output + + # 1st run → creates file + result = run( + [ + "--seeds", + "https://example.com", + "--warcs", + str(TEST_DATA_DIR / "example-response.warc"), + "--output", + str(tmp_path), + "--zim-file", + zim_output, + "--name", + "overwrite-test", + ] + ) + assert result in (None, 100) + assert output_path.exists() + + # 2nd run, no overwrite → should fail + with pytest.raises(SystemExit) as exc: + run( + [ + "--seeds", + "https://example.com", + "--warcs", + str(TEST_DATA_DIR / "example-response.warc"), + "--output", + str(tmp_path), + "--zim-file", + zim_output, + "--name", + "overwrite-test", + ] + ) + assert exc.value.code == 2 + + # 2nd run, no overwrite → should fail + with pytest.raises(SystemExit) as exc: + run( + [ + "--seeds", + "https://example.com", + "--output", + str(tmp_path), + "--zim-file", + zim_output, + "--name", + "overwrite-test", + ] + ) + assert exc.value.code == 2 + + # 3rd run, with overwrite → should succeed + result = run( + [ + "--seeds", + "https://example.com", + "--warcs", + str(TEST_DATA_DIR / "example-response.warc"), + "--output", + str(tmp_path), + "--zim-file", + zim_output, + "--name", + "overwrite-test", + "--overwrite", + ] + ) + assert result in (None, 100) + assert output_path.exists() diff --git a/zimit.ini b/zimit.ini deleted file mode 100644 index 5e0089a..0000000 --- a/zimit.ini +++ /dev/null @@ -1,62 +0,0 @@ -[app:main] -use = egg:zimit - -zimit.zimwriterfs_bin = /home/alexis/dev/openzim/zimwriterfs/zimwriterfs -zimit.httrack_bin = /usr/bin/httrack -zimit.output_location = /home/alexis/dev/zimit/zims -zimit.output_url = http://zimit.notmyidea.org/zims - -mail.host = localhost -mail.port = 2525 -mail.default_sender = zimit@notmyidea.org - -pyramid.includes = - pyramid_mailer - -[server:main] -use = egg:waitress#main -host = 0.0.0.0 -port = 6543 - -# Begin logging configuration - -[uwsgi] -wsgi-file = app.wsgi -http-socket = :8000 -enable-threads = true -master = true -processes = 1 -virtualenv = . -module = zimit -lazy = true -lazy-apps = true - - -[loggers] -keys = root, gplayproxy - -[handlers] -keys = console - -[formatters] -keys = generic - -[logger_root] -level = INFO -handlers = console - -[logger_gplayproxy] -level = DEBUG -handlers = -qualname = gplayproxy - -[handler_console] -class = StreamHandler -args = (sys.stderr,) -level = NOTSET -formatter = generic - -[formatter_generic] -format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s - -# End logging configuration diff --git a/zimit/__init__.py b/zimit/__init__.py deleted file mode 100644 index 9c89766..0000000 --- a/zimit/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from pyramid.config import Configurator -from pyramid.events import NewRequest -from pyramid.static import static_view - -from redis import Redis -from rq import Queue - - -def main(global_config, **settings): - config = Configurator(settings=settings) - config.registry.queue = Queue(connection=Redis()) - - def attach_objects_to_request(event): - event.request.queue = config.registry.queue - - config.add_subscriber(attach_objects_to_request, NewRequest) - - config.include("cornice") - config.include('pyramid_mailer') - config.scan("zimit.views") - - static = static_view('../app', use_subpath=True, index='index.html') - config.add_route('catchall_static', '/app/*subpath') - config.add_view(static, route_name="catchall_static") - return config.make_wsgi_app() diff --git a/zimit/creator.py b/zimit/creator.py deleted file mode 100644 index 9646892..0000000 --- a/zimit/creator.py +++ /dev/null @@ -1,146 +0,0 @@ -import os -import os.path -import shutil -import tempfile -import urlparse - -from slugify import slugify - -from zimit import utils - -HTTRACK_BIN = "/usr/bin/httrack" -DEFAULT_AUTHOR = "ZimIt" - - -class ZimCreator(object): - """A synchronous zim creator, using HTTrack to spider websites and - zimwriterfs to create the zim files. - - Please note that every operation is blocking the interpretor. As such, it - is recommended to run this operation in a worker if invoked from a website - view / controller. - """ - - def __init__(self, zimwriterfs_bin, output_location, - author=DEFAULT_AUTHOR, httrack_bin=HTTRACK_BIN, - log_file=None, max_download_speed=25000): - self.output_location = output_location - self.author = author - self.zimwriterfs_bin = zimwriterfs_bin - self.httrack_bin = httrack_bin - self.log_file = log_file - self.max_download_speed = max_download_speed - - utils.ensure_paths_exists( - self.zimwriterfs_bin, - self.httrack_bin, - self.output_location) - - def _spawn(self, cmd): - return utils.spawn(cmd, self.log_file) - - def download_website(self, url, destination_path): - """Downloads the website using HTTrack and wait for the results to - be available before returning. - - :param url: - The entry URL of the website to retrieve. - - :param destination_path: - The absolute location of a folder where the files will be written. - """ - options = { - "path": destination_path, - "max-rate": self.max_download_speed, - "keep-alive": None, - "robots": 0, - "near": None, - } - - self._spawn(utils.get_command(self.httrack_bin, url, **options)) - - def prepare_website_folder(self, url, input_location): - """Prepare the website files to make them ready to be embedded in a zim - file. - - :returns: - the absolute location of the website folder, ready to be embedded. - """ - netloc = urlparse.urlparse(url).netloc.replace(":", "_") - website_folder = os.path.join(input_location, netloc) - if not os.path.isdir(website_folder): - message = "Unable to find the website folder! %s" % website_folder - raise Exception(message) - shutil.copy('./favicon.ico', website_folder) - return website_folder - - def create_zim(self, input_location, output_name, zim_options): - """Create a zim file out of an existing folder on disk. - - :param input_location: - The absolute location of the files to be bundled in the zim file. - :param output_name: - The name to use to create the zim file. - :param options: - Options to pass to the zim creator. - """ - - zim_options.update({ - 'bin': self.zimwriterfs_bin, - 'location': input_location, - 'output': os.path.join(self.output_location, output_name), - 'icon': 'favicon.ico', - 'publisher': self.author, - }) - - # Spawn zimwriterfs with the correct options. - options = ( - '{bin} -w "{welcome}" -l "{language}" -t "{title}"' - ' -d "{description}" -f {icon} -c "{author}"' - ' -p "{publisher}" {location} {output}' - ).format(**zim_options) - self._spawn(options) - return output_name - - def create_zim_from_website(self, url, zim_options): - """Create a zim file from a website. It might take some time. - - The name of the generated zim file is a slugified version of its URL. - - :param url: - the URL of the website to download. - - :param zim_options: - A dictionary of options to use when generating the Zim file. They - are title, language, welcome and description. - - :returns: - the name of the generated zim_file (relative to the output_folder) - """ - temporary_location = tempfile.mkdtemp("zimit") - self.download_website(url, temporary_location) - website_folder = self.prepare_website_folder(url, temporary_location) - output_name = "{slug}.zim".format(slug=slugify(url)) - zim_file = self.create_zim(website_folder, output_name, zim_options) - return zim_file - - -def load_from_settings(settings, log_file=None): - """Load the ZimCreator object from the given pyramid settings, converting - them to actual parameters. - - This is a convenience function for people wanting to create a ZimCreator - out of a ini file compatible with the pyramid framework. - - :param settings: the dictionary of settings. - """ - if 'zimit.zimwriterfs_bin' not in settings: - raise ValueError('Please define zimit.zimwriterfs_bin config.') - - return ZimCreator( - zimwriterfs_bin=settings['zimit.zimwriterfs_bin'], - httrack_bin=settings.get('zimit.httrack_bin'), - output_location=settings.get('zimit.output_location'), - author=settings.get('zimit.default_author'), - log_file=log_file - ) diff --git a/zimit/mailer.py b/zimit/mailer.py deleted file mode 100644 index 4723304..0000000 --- a/zimit/mailer.py +++ /dev/null @@ -1,42 +0,0 @@ -from pyramid_mailer.message import Attachment, Message -from pyramid_mailer import Mailer - - -def send_zim_url(settings, email, zim_url): - """Send an email with a link to one zim file. - - :param settings: - A pyramid settings object, used by pyramid_mailer. - :param email: - The email of the recipient. - :param zim_url: - The URL of the zim file. - """ - mailer = Mailer.from_settings(settings) - msg = ZimReadyMessage(email, zim_url) - mailer.send_immediately(msg) - - -class ZimReadyMessage(Message): - def __init__(self, email, zim_link): - subject = "[ZimIt!] Your zimfile is ready!" - - bdata = """ -Hi, - -You have asked for the creation of a zim file, and it is now ready ! - -You can access it at the following URL: - - {zim_link} - -Cheers, -ZimIt. -""".format(zim_link=zim_link) - hdata = bdata - - body = Attachment(data=bdata, transfer_encoding="quoted-printable") - html = Attachment(data=hdata, transfer_encoding="quoted-printable") - - super(ZimReadyMessage, self).__init__( - subject=subject, body=body, html=html, recipients=[email]) diff --git a/zimit/utils.py b/zimit/utils.py deleted file mode 100644 index b782824..0000000 --- a/zimit/utils.py +++ /dev/null @@ -1,35 +0,0 @@ -import os -import shlex -import subprocess - - -def spawn(cmd, logfile=None): - """Quick shortcut to spawn a command on the filesystem""" - if logfile is not None: - with open(logfile, "a+") as f: - prepared_cmd = shlex.split("stdbuf -o0 %s" % cmd) - process = subprocess.Popen(prepared_cmd, stdout=f) - else: - prepared_cmd = shlex.split(cmd) - process = subprocess.Popen(prepared_cmd) - process.wait() - return process - - -def ensure_paths_exists(*paths): - for path in paths: - if not os.path.exists(path): - msg = '%s does not exist.' % path - raise OSError(msg) - - -def get_command(cmd, *params, **options): - prepared_options = [] - for key, value in options.items(): - if value is None: - opt = "--%s" % key - else: - opt = "--%s=%s" % (key, value) - prepared_options.append(opt) - - return " ".join((cmd, " ".join(params), " ".join(prepared_options))) diff --git a/zimit/views.py b/zimit/views.py deleted file mode 100644 index 034cb05..0000000 --- a/zimit/views.py +++ /dev/null @@ -1,63 +0,0 @@ -import os - -from cornice import Service -from colander import MappingSchema, SchemaNode, String -from pyramid.httpexceptions import HTTPTemporaryRedirect, HTTPNotFound - -from zimit.worker import create_zim - -website = Service(name='website', path='/website-zim') -home = Service(name='home', path='/') -status = Service(name='status', path='/status/{id}') - - -@home.get() -def redirect_to_app(request): - raise HTTPTemporaryRedirect("/app/index.html") - - -class WebSiteSchema(MappingSchema): - url = SchemaNode(String(), location="body", type='str') - title = SchemaNode(String(), location="body", type='str') - email = SchemaNode(String(), location="body", type='str') - description = SchemaNode(String(), default="-", - location="body", type='str') - author = SchemaNode(String(), default=None, - location="body", type='str') - welcome = SchemaNode(String(), default="index.html", - location="body", type='str') - language = SchemaNode(String(), default="eng", - location="body", type='str') - - -@website.post(schema=WebSiteSchema) -def crawl_new_website(request): - job = request.queue.enqueue( - create_zim, - request.registry.settings, - request.validated, - timeout=1800) - request.response.status_code = 201 - return { - 'job_id': job.id - } - - -@status.get() -def display_status(request): - job = request.queue.fetch_job(request.matchdict["id"]) - if job is None: - raise HTTPNotFound() - - log_dir = request.registry.settings.get('zimit.logdir', '/tmp') - log_file = os.path.join(log_dir, "%s.log" % job.id) - - log_content = None - if os.path.exists(log_file): - with open(log_file) as f: - log_content = f.read() - - return { - "status": job.status, - "log": log_content - } diff --git a/zimit/worker.py b/zimit/worker.py deleted file mode 100644 index 6a71cb9..0000000 --- a/zimit/worker.py +++ /dev/null @@ -1,20 +0,0 @@ -import os -import urlparse - -from rq import get_current_job - -from zimit.mailer import send_zim_url -from zimit.creator import load_from_settings - - -def create_zim(settings, options): - """Call the zim creator and the mailer when it is finished. - """ - job = get_current_job() - log_dir = settings.get('zimit.logdir', '/tmp') - log_file = os.path.join(log_dir, "%s.log" % job.id) - zim_creator = load_from_settings(settings, log_file) - zim_file = zim_creator.create_zim_from_website(options['url'], options) - output_url = settings.get('zimit.output_url') - zim_url = urlparse.urljoin(output_url, zim_file) - send_zim_url(settings, options['email'], zim_url)