├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── general_question.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── build.yaml │ ├── docker_compose_build.yml │ ├── publish_docs.yaml │ ├── release.yaml │ └── run_precommit.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.rst ├── CODE_OF_CONDUCT.rst ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.rst ├── deployment ├── .env ├── docker-compose.yml ├── nginx-selfsigned.crt ├── nginx-selfsigned.key └── nginx.conf ├── docs ├── Makefile ├── make.bat ├── requirements.txt └── source │ ├── _static │ ├── css │ │ ├── images │ │ │ ├── overlay.png │ │ │ └── shadow.png │ │ └── main.css │ ├── images │ │ ├── MegaQC_logo.ai │ │ ├── MegaQC_logo.png │ │ ├── MegaQC_logo_darkbg.ai │ │ ├── MegaQC_logo_darkbg.png │ │ ├── MegaQC_logo_darkbg.svg │ │ └── header_bg.jpg │ └── megaqc_style.css │ ├── _templates │ ├── LICENSE.txt │ ├── README.txt │ └── index.html │ ├── api_reference.rst │ ├── cli.rst │ ├── conf.py │ ├── docs │ ├── changelog.rst │ ├── code_of_conduct.rst │ ├── dev │ │ ├── backend.rst │ │ ├── development.rst │ │ ├── documentation.rst │ │ └── frontend.rst │ ├── index.rst │ ├── installation │ │ ├── installation.rst │ │ ├── installation_dev.rst │ │ ├── installation_docker.rst │ │ ├── installation_prod.rst │ │ └── migrations.rst │ ├── troubleshooting.rst │ └── usage │ │ ├── usage.rst │ │ ├── usage_admin.rst │ │ └── usage_setup.rst │ ├── images │ └── megaqc_homepage.png │ ├── megaqc.api.rst │ ├── megaqc.model.rst │ ├── megaqc.public.rst │ ├── megaqc.report_plot.rst │ ├── megaqc.rest_api.rst │ ├── megaqc.rst │ ├── megaqc.user.rst │ ├── megaqc.utils.rst │ └── modules.rst ├── logo ├── MegaQC_logo.ai ├── MegaQC_logo.png ├── MegaQC_logo.svg ├── MegaQC_logo_darkbg.ai └── MegaQC_logo_darkbg.png ├── megaqc ├── __init__.py ├── api │ ├── __init__.py │ ├── constants.py │ ├── utils.py │ └── views.py ├── app.py ├── cli.py ├── commands.py ├── compat.py ├── database.py ├── extensions.py ├── migrations │ ├── README │ ├── alembic.ini │ ├── env.py │ └── script.py.mako ├── model │ ├── __init__.py │ └── models.py ├── public │ ├── __init__.py │ ├── forms.py │ └── views.py ├── report_plot │ ├── __init__.py │ └── views.py ├── rest_api │ ├── __init__.py │ ├── content.py │ ├── fields.py │ ├── filters.py │ ├── outlier.py │ ├── plot.py │ ├── schemas.py │ ├── utils.py │ ├── views.py │ └── webarg_parser.py ├── scheduler.py ├── settings.py ├── static │ ├── css │ │ ├── bootstrap.min.css │ │ ├── component-chosen.min.css │ │ ├── font-awesome.min.css │ │ ├── gridstack.min.css │ │ ├── main.css │ │ ├── tether.min.css │ │ └── toastr.min.css │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ ├── fontawesome-webfont.woff2 │ │ └── roboto │ │ │ ├── LICENSE.txt │ │ │ ├── Roboto-Black.ttf │ │ │ ├── Roboto-BlackItalic.ttf │ │ │ ├── Roboto-Bold.ttf │ │ │ ├── Roboto-BoldItalic.ttf │ │ │ ├── Roboto-Italic.ttf │ │ │ ├── Roboto-Light.ttf │ │ │ ├── Roboto-LightItalic.ttf │ │ │ ├── Roboto-Medium.ttf │ │ │ ├── Roboto-MediumItalic.ttf │ │ │ ├── Roboto-Regular.ttf │ │ │ ├── Roboto-Thin.ttf │ │ │ └── Roboto-ThinItalic.ttf │ ├── img │ │ ├── BarRandom-256.png │ │ ├── MegaQC_logo.png │ │ ├── MegaQC_logo.svg │ │ ├── SciLifeLab_logo.png │ │ ├── comparisons.png │ │ ├── distributions.png │ │ ├── favicon.png │ │ ├── favicon.svg │ │ ├── report_plot_thumb.svg │ │ └── trends.png │ └── js │ │ ├── filter_samples.js │ │ ├── libs │ │ ├── bootstrap.min.js │ │ ├── bootstrap3-typeahead.min.js │ │ ├── chosen.jquery.min.js │ │ ├── clipboard.min.js │ │ ├── gridstack.jQueryUI.min.js │ │ ├── gridstack.min.js │ │ ├── jquery-3.2.1.min.js │ │ ├── jquery-ui.min.js │ │ ├── lodash.min.js │ │ ├── popper.min.js │ │ └── toastr.min.js │ │ ├── megaqc.js │ │ ├── members.js │ │ ├── plot_choice.js │ │ └── user_admin.js ├── templates │ ├── 401.html │ ├── 404.html │ ├── 500.html │ ├── error.html │ ├── footer.html │ ├── layout.html │ ├── layout_blank.html │ ├── nav.html │ ├── public │ │ ├── about.html │ │ ├── comparisons.html │ │ ├── dashboard.html │ │ ├── distributions.html │ │ ├── filter_samples_modal.html │ │ ├── filter_samples_selectbox.html │ │ ├── home.html │ │ ├── login.html │ │ ├── plot_choice.html │ │ ├── plot_type.html │ │ ├── react.html │ │ ├── register.html │ │ ├── report_plot.html │ │ ├── reports_management.html │ │ └── save_plot_favourite_modal.html │ └── users │ │ ├── change_password.html │ │ ├── create_dashboard.html │ │ ├── dashboards.html │ │ ├── manage_users.html │ │ ├── multiqc_config.html │ │ ├── organize_filters.html │ │ ├── plot_favourite.html │ │ ├── plot_favourites.html │ │ ├── profile.html │ │ └── queued_uploads.html ├── user │ ├── __init__.py │ ├── forms.py │ ├── models.py │ └── views.py ├── utils │ ├── __init__.py │ ├── config_defaults.yaml │ └── settings.py └── wsgi.py ├── package-lock.json ├── package.json ├── poetry.lock ├── prestart.sh ├── pyproject.toml ├── requirements.txt ├── setup.py ├── src ├── admin.js ├── admin │ ├── components │ │ ├── defaultForm.js │ │ ├── jsonField.js │ │ ├── layout.js │ │ └── resourceLink.js │ ├── dashboards.js │ ├── dataType.js │ ├── favourite.js │ ├── filterGroup.js │ ├── meta.js │ ├── report.js │ ├── sample.js │ ├── sampleData.js │ ├── upload.js │ └── user.js ├── trend.js ├── trend │ ├── DateRangeField.js │ ├── bootstrapField.js │ ├── bootstrapHookField.js │ ├── filterRow.js │ ├── form.js │ ├── newFilter.js │ ├── outlierDetection.js │ ├── plot.js │ ├── sampleFilter.js │ ├── savePlot.js │ └── trendSchema.json └── util │ ├── api.js │ ├── autoSave.js │ ├── filter.js │ └── filterSchema.js ├── start.sh ├── tests ├── __init__.py ├── api │ ├── __init__.py │ ├── conftest.py │ ├── test_api.py │ ├── test_build_filter.py │ ├── test_current_user.py │ ├── test_filter.py │ ├── test_permissions.py │ ├── test_plot.py │ ├── test_upload.py │ └── utils.py ├── conftest.py ├── factories.py ├── multiqc_data.json ├── test_config.py ├── test_docker.py ├── test_forms.py ├── test_functional.py └── test_models.py ├── uploads └── README.md └── webpack.config.js /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug in MegaQC 4 | title: Bug report 5 | labels: bug 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | 11 | 12 | 13 | **To Reproduce** 14 | 15 | Steps to reproduce the behavior: 16 | 17 | 1. Run megaqc with command: '...' 18 | 2. Select: '...' 19 | 3. See error 20 | 21 | **Expected behavior** 22 | 23 | 24 | 25 | 35 | 36 | **Additional context** 37 | 38 | 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature 4 | title: Feature Request Summary 5 | labels: enhancement 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | 11 | 12 | 13 | **Describe the solution you would like** 14 | 15 | 16 | 17 | **Additional context** 18 | 19 | 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/general_question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: General question 3 | about: Ask a question about anything MegaQC 4 | title: Question about MegaQC 5 | labels: question 6 | assignees: "" 7 | --- 8 | 9 | **Question** 10 | 11 | 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "npm" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | 8 | - package-ecosystem: "pip" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | 13 | - package-ecosystem: github-actions 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **PR Checklist** 4 | 5 | 6 | 7 | - [ ] This comment contains a description of changes (with reason) 8 | - [ ] Referenced issue is linked 9 | - [ ] If you've fixed a bug or added code that should be tested, add tests! 10 | - [ ] Documentation in `docs` is updated 11 | - [ ] `docs/changelog.md` is updated 12 | 13 | **Description of changes** 14 | 15 | 16 | 17 | **Technical details** 18 | 19 | 20 | 21 | **Additional context** 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Python tests and Node.js build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build-python: 7 | runs-on: ubuntu-22.04 8 | 9 | strategy: 10 | matrix: 11 | python-version: ["3.8", "3.9", "3.10", "3.11"] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v2.1.4 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | 21 | - uses: snok/install-poetry@v1.3.3 22 | 23 | - name: Install all extra features of poetry 24 | run: poetry install --extras "dev" 25 | 26 | - name: Test with pytest 27 | run: poetry run pytest 28 | 29 | build-node: 30 | env: 31 | # Webpack uses around 2GB to build, which is more than the default heap size. We bump it to 4 GB here just in case 32 | NODE_OPTIONS: "--max-old-space-size=4096" 33 | 34 | runs-on: ubuntu-latest 35 | 36 | strategy: 37 | matrix: 38 | node-version: [10.x, 12.x, 14.x, 15.x] 39 | 40 | steps: 41 | - uses: actions/checkout@v2 42 | 43 | - name: Use Node.js ${{ matrix.node-version }} 44 | uses: actions/setup-node@v2.1.3 45 | with: 46 | node-version: ${{ matrix.node-version }} 47 | 48 | - name: Install dependencies 49 | run: npm install 50 | 51 | - name: Test build 52 | run: npm run build 53 | -------------------------------------------------------------------------------- /.github/workflows/docker_compose_build.yml: -------------------------------------------------------------------------------- 1 | name: Docker Compose Test 2 | 3 | on: push 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Build the stack 11 | run: | 12 | cd deployment 13 | docker-compose up -d 14 | 15 | - uses: jakejarvis/wait-action@master 16 | with: 17 | time: "60" 18 | 19 | - name: Check running containers 20 | run: | 21 | cd deployment 22 | docker-compose logs 23 | 24 | # TODO It would be nice to upload an example report as a test 25 | - name: Run MegaQC Help 26 | run: docker exec deployment_megaqc_1 megaqc --help 27 | -------------------------------------------------------------------------------- /.github/workflows/publish_docs.yaml: -------------------------------------------------------------------------------- 1 | name: Publish documentation 2 | 3 | on: 4 | push: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')" 10 | 11 | steps: 12 | - uses: actions/checkout@v2 13 | name: Check out source-code repository 14 | 15 | - name: Setup Python 16 | uses: actions/setup-python@v2.1.4 17 | with: 18 | python-version: 3.9 19 | 20 | - name: Install pip 21 | run: python -m pip install --upgrade pip 22 | 23 | # Required to automatically extract Click CLI and Flask API documentation 24 | - name: Install MegaQC 25 | run: pip install -e .[dev] 26 | 27 | - name: Install documentation dependencies 28 | run: pip install -r docs/requirements.txt 29 | 30 | - name: Create API documentation 31 | run: | 32 | cd docs 33 | make api-docs 34 | 35 | - name: Build documentation 36 | run: | 37 | cd docs 38 | make html 39 | echo "megaqc.info" > _build/html/CNAME 40 | 41 | # TODO: We could also build latex & PDF documentation and upload them as artifacts 42 | 43 | - name: Deploy 44 | if: ${{ github.ref == 'refs/heads/main' }} 45 | uses: peaceiris/actions-gh-pages@v3 46 | with: 47 | github_token: ${{ secrets.GITHUB_TOKEN }} 48 | publish_dir: ./docs/_build/html 49 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | # This is a separate workflow because we don't want a matrix build for the release (we only want one package) 2 | name: Publish Release 3 | 4 | # Whenever we publish a release 5 | on: 6 | release: 7 | types: [created] 8 | 9 | jobs: 10 | deploy: 11 | env: 12 | # Webpack uses around 2GB to build, which is more than the default heap size. We bump it to 4 GB here just in case 13 | NODE_OPTIONS: "--max-old-space-size=4096" 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - name: Set up Python 21 | uses: actions/setup-python@v2.1.4 22 | with: 23 | python-version: "3.x" 24 | 25 | - name: Set up Node 26 | uses: actions/setup-node@v2.1.3 27 | with: 28 | node-version: "12.x" 29 | 30 | - name: Install Python dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | pip install -e .[dev,deploy] 34 | 35 | - name: Install node dependencies 36 | run: npm install 37 | 38 | - name: Build JavaScript 39 | run: npm run build --if-present 40 | 41 | - name: Build python package 42 | run: | 43 | python setup.py sdist bdist_wheel 44 | 45 | - name: Upload to existing release 46 | uses: AButler/upload-release-assets@v2.0 47 | with: 48 | files: "dist/*" 49 | repo-token: ${{ secrets.GITHUB_TOKEN }} 50 | 51 | - name: Publish to PyPI 52 | uses: pypa/gh-action-pypi-publish@master 53 | with: 54 | user: __token__ 55 | password: ${{ secrets.PYPI_PASSWORD }} 56 | -------------------------------------------------------------------------------- /.github/workflows/run_precommit.yml: -------------------------------------------------------------------------------- 1 | name: Run precommit 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-python@v2.1.4 11 | with: 12 | python-version: "3.9" 13 | 14 | - name: Install pre-commit 15 | run: pip install pre-commit 16 | 17 | - name: Lint using pre-commit 18 | run: | 19 | pre-commit run --all-files 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | megaqc/static/js/admin* 3 | megaqc/static/js/trend* 4 | venv/ 5 | *.db 6 | 7 | # Uploads 8 | uploads/* 9 | !uploads/README.md 10 | 11 | # Development stuff 12 | dev.db 13 | test.db 14 | megaqc.db 15 | dist/ 16 | 17 | # Dev CSS 18 | megaqc/static/scss/*.css 19 | 20 | # Front end packaging stuff 21 | .sass-cache 22 | _build 23 | node_modules 24 | 25 | .DS_Store 26 | 27 | *.py[cod] 28 | 29 | # C extensions 30 | *.so 31 | 32 | # Packages 33 | *.egg 34 | *.egg-info 35 | build 36 | eggs 37 | parts 38 | bin 39 | var 40 | sdist 41 | develop-eggs 42 | .installed.cfg 43 | lib 44 | lib64 45 | 46 | # Installer logs 47 | pip-log.txt 48 | 49 | # Unit test / coverage reports 50 | .coverage 51 | .tox 52 | nosetests.xml 53 | 54 | # Translations 55 | *.mo 56 | 57 | # Mr Developer 58 | .mr.developer.cfg 59 | .project 60 | .pydevproject 61 | 62 | # Complexity 63 | output/*.html 64 | output/*/index.html 65 | 66 | # Virtualenvs 67 | env/ 68 | 69 | # futurize backups 70 | *.py.bak 71 | 72 | # VScode 73 | .vscode -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/timothycrosley/isort 3 | rev: 5.12.0 4 | hooks: 5 | - id: isort 6 | types: [python] 7 | args: 8 | - "--multi-line=3" 9 | - "--trailing-comma" 10 | - "--force-grid-wrap=0" 11 | - "--use-parentheses" 12 | - "--line-width=88" 13 | 14 | - repo: https://github.com/myint/docformatter 15 | rev: v1.7.1 16 | hooks: 17 | - id: docformatter 18 | args: 19 | - "--black" 20 | - "--make-summary-multi-line" 21 | - "--pre-summary-newline" 22 | - "--in-place" 23 | - "--recursive" 24 | 25 | - repo: https://github.com/psf/black 26 | rev: "23.3.0" 27 | hooks: 28 | - id: black 29 | 30 | - repo: https://github.com/pre-commit/mirrors-prettier 31 | rev: v2.7.1 32 | hooks: 33 | - id: prettier 34 | exclude: (megaqc/templates|megaqc/static|docs/source) 35 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.rst: -------------------------------------------------------------------------------- 1 | Contributor Covenant Code of Conduct 2 | ==================================== 3 | 4 | Our Pledge 5 | ---------- 6 | 7 | In the interest of fostering an open and welcoming environment, we as 8 | contributors and maintainers pledge to making participation in our 9 | project and our community a harassment-free experience for everyone, 10 | regardless of age, body size, disability, ethnicity, gender identity and 11 | expression, level of experience, nationality, personal appearance, race, 12 | religion, or sexual identity and orientation. 13 | 14 | Our Standards 15 | ------------- 16 | 17 | Examples of behavior that contributes to creating a positive environment 18 | include: 19 | 20 | - Using welcoming and inclusive language 21 | - Being respectful of differing viewpoints and experiences 22 | - Gracefully accepting constructive criticism 23 | - Focusing on what is best for the community 24 | - Showing empathy towards other community members 25 | 26 | Examples of unacceptable behavior by participants include: 27 | 28 | - The use of sexualized language or imagery and unwelcome sexual 29 | attention or advances 30 | - Trolling, insulting/derogatory comments, and personal or political 31 | attacks 32 | - Public or private harassment 33 | - Publishing others’ private information, such as a physical or 34 | electronic address, without explicit permission 35 | - Other conduct which could reasonably be considered inappropriate in a 36 | professional setting 37 | 38 | Our Responsibilities 39 | -------------------- 40 | 41 | Project maintainers are responsible for clarifying the standards of 42 | acceptable behavior and are expected to take appropriate and fair 43 | corrective action in response to any instances of unacceptable behavior. 44 | 45 | Project maintainers have the right and responsibility to remove, edit, 46 | or reject comments, commits, code, wiki edits, issues, and other 47 | contributions that are not aligned to this Code of Conduct, or to ban 48 | temporarily or permanently any contributor for other behaviors that they 49 | deem inappropriate, threatening, offensive, or harmful. 50 | 51 | Scope 52 | ----- 53 | 54 | This Code of Conduct applies both within project spaces and in public 55 | spaces when an individual is representing the project or its community. 56 | Examples of representing a project or community include using an 57 | official project e-mail address, posting via an official social media 58 | account, or acting as an appointed representative at an online or 59 | offline event. Representation of a project may be further defined and 60 | clarified by project maintainers. 61 | 62 | Enforcement 63 | ----------- 64 | 65 | Instances of abusive, harassing, or otherwise unacceptable behavior may 66 | be reported by contacting the project team. The project team 67 | will review and investigate all complaints, and will respond in a way 68 | that it deems appropriate to the circumstances. The project team is 69 | obligated to maintain confidentiality with regard to the reporter of an 70 | incident. Further details of specific enforcement policies may be posted 71 | separately. 72 | 73 | Project maintainers who do not follow or enforce the Code of Conduct in 74 | good faith may face temporary or permanent repercussions as determined 75 | by other members of the project’s leadership. 76 | 77 | Attribution 78 | ----------- 79 | 80 | This Code of Conduct is adapted from the `Contributor Covenant`_, 81 | version 1.4, available at 82 | `https://contributor-covenant.org/version/1/4`_ 83 | 84 | .. _Contributor Covenant: https://contributor-covenant.org 85 | .. _`https://contributor-covenant.org/version/1/4`: https://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Compile the JS in an isolated container 2 | FROM node:16 3 | COPY . /app 4 | WORKDIR /app 5 | RUN npm install 6 | RUN npm run build 7 | 8 | # Setup the final container that will we run 9 | FROM tiangolo/meinheld-gunicorn-flask:python3.8 10 | LABEL authors="phil.ewels@scilifelab.se,denis.moreno@scilifelab.se" \ 11 | description="Docker image running MegaQC with Gunicorn" 12 | 13 | # Tell MegaQC to use postgres / psycopg2 14 | ENV MEGAQC_PRODUCTION=1 \ 15 | MEGAQC_SECRET="SuperSecretValueYouShouldReallyChange" \ 16 | MEGAQC_CONFIG="" \ 17 | APP_MODULE=megaqc.wsgi:app\ 18 | DB_HOST="127.0.0.1" \ 19 | DB_PORT="5432" \ 20 | DB_NAME="megaqc" \ 21 | DB_USER="megaqc" \ 22 | DB_PASS="megaqcpswd" 23 | COPY . /app 24 | 25 | # Patch in a custom start script 26 | COPY ./start.sh /start.sh 27 | RUN chmod +x /start.sh 28 | 29 | # Copy the compiled JS in from the other node container 30 | COPY --from=0 /app/megaqc/static/ /app/megaqc/static/ 31 | RUN pip install /app[prod] -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | recursive-include megaqc * 4 | recursive-include uploads * 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | |MegaQC| 2 | 3 | A web application to collect and visualise data from multiple MultiQC reports. 4 | 5 | |Docker| |Build Status| |Gitter| |Documentation| |PyPI| 6 | 7 | Current Status: *“Pretty unstable”* 8 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 9 | 10 | As of mid-October 2017, MegaQC has all basic functionality. We’ve made 11 | the repository public, but please bear in mind that it is still under heavy 12 | development and changes are being made on a daily basis. It’s safe to 13 | assume that the database structure is still at risk and that you 14 | shouldn’t yet trust it to be stable. However, we’d love your help in 15 | testing, bug finding and development! 16 | 17 | -------------- 18 | 19 | MegaQC is a web application that you can install and run on your own 20 | network. It collects and visualises data parsed by MultiQC across multiple runs. 21 | The MegaQC home page looks something like this: 22 | 23 | .. figure:: https://raw.githubusercontent.com/MultiQC/MegaQC/main/docs/source/images/megaqc_homepage.png 24 | :alt: MegaQC homepage 25 | 26 | Screenshot of the MegaQC home page. 27 | 28 | If you are not sure what MultiQC is yet, check out the main `MultiQC 29 | website`_ and `GitHub repository`_ first. Once MegaQC is installed and running, 30 | simply configure MultiQC to automatically save data to the website every time it runs. 31 | Users of your group or facility can then replicate MultiQC plots and explore different data 32 | fields. Data distributions, timelines and comparisons can all be explored. 33 | 34 | Please read the `MegaQC Documentation `_ 35 | to learn how to install, deploy and use MegaQC. 36 | 37 | .. _MultiQC website: http://multiqc.info 38 | .. _GitHub repository: https://github.com/ewels/MultiQC 39 | 40 | .. |MegaQC| image:: https://raw.githubusercontent.com/MultiQC/MegaQC/main/megaqc/static/img/MegaQC_logo.png 41 | .. |Docker| image:: https://img.shields.io/docker/automated/multiqc/megaqc.svg?style=flat-square 42 | :target: https://hub.docker.com/r/multiqc/megaqc/ 43 | .. |Gitter| image:: https://img.shields.io/badge/gitter-%20join%20chat%20%E2%86%92-4fb99a.svg?style=flat-square 44 | :target: https://gitter.im/ewels/MegaQC 45 | .. |Documentation| image:: https://img.shields.io/badge/Documentation-passing-passing 46 | :target: https://megaqc.info/docs/index.html 47 | .. |PyPI| image:: https://img.shields.io/pypi/v/megaqc?color=passing 48 | :target: https://pypi.org/project/megaqc/ 49 | -------------------------------------------------------------------------------- /deployment/.env: -------------------------------------------------------------------------------- 1 | MEGAQC_PRODUCTION=1 2 | MEGAQC_SECRET=SuperSecretValueYouShouldReallyChange 3 | APP_MODULE=megaqc.wsgi:app 4 | DB_HOST=db 5 | DB_PORT=5432 6 | DB_NAME=megaqc 7 | DB_USER=megaqc 8 | DB_PASS=megaqcpswd 9 | CRT_PATH=./nginx-selfsigned.crt 10 | KEY_PATH=./nginx-selfsigned.key 11 | -------------------------------------------------------------------------------- /deployment/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | megaqc: 5 | image: multiqc/megaqc 6 | build: .. 7 | volumes: 8 | # Share the static files via a volume 9 | - static_volume:/app/megaqc/static 10 | depends_on: 11 | - db 12 | environment: 13 | - MEGAQC_PRODUCTION=${MEGAQC_PRODUCTION} 14 | - MEGAQC_SECRET=${MEGAQC_SECRET} 15 | - APP_MODULE=${APP_MODULE} 16 | - DB_HOST=${DB_HOST} 17 | - DB_PORT=${DB_PORT} 18 | - DB_NAME=${DB_NAME} 19 | - DB_USER=${DB_USER} 20 | - DB_PASS=${DB_PASS} 21 | db: 22 | image: postgres:latest 23 | volumes: 24 | - db_volume:/var/lib/postgresql/data/ 25 | environment: 26 | POSTGRES_HOST_AUTH_METHOD: trust 27 | nginx: 28 | image: nginx:latest 29 | volumes: 30 | # Mount the nginx conf 31 | - ./nginx.conf:/etc/nginx/nginx.conf:ro 32 | # Mount the self-signed certs 33 | - ${CRT_PATH}:/etc/nginx/megaqc.crt:ro 34 | - ${KEY_PATH}:/etc/nginx/megaqc.key:ro 35 | # Mount the static files from MegaQC so we can serve them efficiently 36 | - static_volume:/home/app/web/project/static 37 | ports: 38 | - 80:80 39 | - 443:443 40 | depends_on: 41 | - megaqc 42 | 43 | volumes: 44 | static_volume: 45 | db_volume: 46 | -------------------------------------------------------------------------------- /deployment/nginx-selfsigned.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDPzCCAiegAwIBAgIUIQHWYEPaIkzdmi/wBnAav4tFyWcwDQYJKoZIhvcNAQEL 3 | BQAwLzELMAkGA1UEBhMCVVMxDzANBgNVBAoMBk1lZ2FRQzEPMA0GA1UEAwwGTWVn 4 | YVFDMB4XDTIwMTEyNzA3MTQxM1oXDTMwMTEyNTA3MTQxM1owLzELMAkGA1UEBhMC 5 | VVMxDzANBgNVBAoMBk1lZ2FRQzEPMA0GA1UEAwwGTWVnYVFDMIIBIjANBgkqhkiG 6 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEApvel62LfUWON2kQXT5GCoiVAJ1aaXxwV8+Tg 7 | rky+KuXS9J7hgIk3v9mhnSBWKFOKc2Sdxg72C4wMp1ELDZgBRC10U8Cj1wFuW30d 8 | 8UBy/+hpHylNN0H6prOmlkRAQTv3Zb1i7xJkNehEHeaBtZlJ0k59U+w43oUWV+5Y 9 | 6jfFRlWRbMiSsHtZ2+84OHEeWgMOkTxyOCSrMeHNYL3Dh1AHzVDf5/2n8mPplurW 10 | MMwiBTOCorPZKh2fwhWUgaIwsxZ/9UMPrDzfib9k7TtjLTnAqNhYemwDo/qG5CQ2 11 | cs1I6qFUDDA7Yx33m9g3I9UuFjSyx4Qanh8Z/2RnYu8jG7NVbwIDAQABo1MwUTAd 12 | BgNVHQ4EFgQU4nBI9eZQaUnLkSQS+DQqqsDdkGAwHwYDVR0jBBgwFoAU4nBI9eZQ 13 | aUnLkSQS+DQqqsDdkGAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC 14 | AQEAMVMglMedhb9R++/mZFU6Bk9CsIdy2hWRbtAJG5o6z08JlwYmiNV12KQ5hESm 15 | LvagVgzTxg/qkwZGtThyKNbslHlLXxF6T600HTNhfayp3tLu3Rv6FmewJvk3kW5C 16 | LRWk0xWLhB4WaRKCuuiYXdb4drVwBsfv3gd+yR2oU/RzoMZI0pSdEXdEQo+5H7SC 17 | xeyNqAwZ4u8vSxfHMPFAJisQFCBAGwlPaAmEks5KH2gtAQEEq+qIx3k63A0huYNL 18 | Ri10ctZQRV7iUJJjqCP8vNe4/t3bKHKjdXD44jDFInFsZraNFeSjh47i6BFg0nQu 19 | ZfTbfaLH6ZjKAlPkkRd21oY/Ow== 20 | -----END CERTIFICATE----- 21 | -------------------------------------------------------------------------------- /deployment/nginx-selfsigned.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCm96XrYt9RY43a 3 | RBdPkYKiJUAnVppfHBXz5OCuTL4q5dL0nuGAiTe/2aGdIFYoU4pzZJ3GDvYLjAyn 4 | UQsNmAFELXRTwKPXAW5bfR3xQHL/6GkfKU03Qfqms6aWREBBO/dlvWLvEmQ16EQd 5 | 5oG1mUnSTn1T7DjehRZX7ljqN8VGVZFsyJKwe1nb7zg4cR5aAw6RPHI4JKsx4c1g 6 | vcOHUAfNUN/n/afyY+mW6tYwzCIFM4Kis9kqHZ/CFZSBojCzFn/1Qw+sPN+Jv2Tt 7 | O2MtOcCo2Fh6bAOj+obkJDZyzUjqoVQMMDtjHfeb2Dcj1S4WNLLHhBqeHxn/ZGdi 8 | 7yMbs1VvAgMBAAECggEBAJ4nqpSCvUIc6Ps80E/gZzsryc75a5R3yBuUS3cO3ntH 9 | 40EvVi8oyxQBE+MABFyCdXzEa+Z1uX5KlJnQXIXt3BU2cR/8M7Wvd1dV9ozVHZlm 10 | Dl0kBC45YzGQrlVcJPkl0jwe3p3Sb/XYIt2nwGnB33lEO9bIxT2SFw4HiVWiqkzp 11 | rCzv6lOpp7RULa4h/2OarwS8/QXWD4bIJglG8gBuKJMkE4Eh8Oq/wqNTjdZ/t+t7 12 | TPjhSsYjYYUQZ6H68I4kbKf4+0MtTo+7diJyJ/x3Ipk5uao6xhmwUILERX1PsN7r 13 | Sqy0CdsD3J+/lpHYlMzUf2NjosgndnvLErPy3zMDbDECgYEA3J4rkDXocoKcrrTh 14 | NjqejIk8hu719RqppFA7yXsK3iA1HsqgiCels6B8GllwIsLlummY/AjGqKdl/oWz 15 | 6/o3K+kOO/iaEeB8/P/2AYtkjbNx6JgsQ9EVMscynJRiGCBiiAtCHk9+ceYKHEKd 16 | Ce+R07C9s2LxapaU7rk5prLnSPMCgYEAwb7E5gapMQrFm986cBqZs/T2yBkLY5oi 17 | 4uEHrLyWrcYphnwyrm7R2d9plzRtmI/kn0n+v7RvxHhH7RHiO5y8R9TKqFnoMTb4 18 | mRNAG/xyDrVhWDRxlJqg3RpLA1/+R3PASYquGdjzQzgLINp559aHgTS8EXTBKvAo 19 | JgAY0xwKoJUCgYEA1XUsjUG7gGAjnV5hsYiSM0PtjljTHa4IiXUgA9fLMhRTkVFY 20 | daR2zQ/wQ8ZZmyicAft+DA2puTEOnG9dIGteyluabCgjWjysclZt/hyS1A/VnMjv 21 | u0YnjpPxwlJHfzSGYT8TYCRmIWoESkehjiWAISSWx2RqVUkRHNGWmFXtNkUCgYEA 22 | mMAlUQ4zWr0Za3kmA7iVMjKSVtSYDCE8HBA7Es9lJQC1QJivBKPOw63G/DWTqtLX 23 | 91ZJiS9jZ01Ft/1/lmxObXuxg+XOkO68NUPeKbJw4nPSmc5PNlBrP6c5OSH3wrHa 24 | CqFR/oLz2C1ZaL+XmVekYhQHquOYYJgTr1IoQFk9b7kCgYBLs8Kn6MLibtxRBKsy 25 | nSA9TJuYNvC7PE0+PwExglZWhIARZC87GuhXwLPNZUUUvDLagBlVhgLGxx4gt2uW 26 | ojtpcj3qypKz3vemWy/kYEQuuK8Q90hj3rBQJZsN5yRzz2ZdXY/HQOfLiNdItdsT 27 | 2iD+K96cFBWOI/kg/DL8Fj0oiw== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /deployment/nginx.conf: -------------------------------------------------------------------------------- 1 | events {} 2 | http { 3 | underscores_in_headers on; 4 | include mime.types; 5 | upstream megaqc { 6 | server megaqc; 7 | } 8 | 9 | server { 10 | listen 80; 11 | listen 443 ssl; 12 | ssl_certificate megaqc.crt; 13 | ssl_certificate_key megaqc.key; 14 | 15 | location / { 16 | proxy_pass http://megaqc; 17 | proxy_redirect off; 18 | 19 | proxy_set_header Host $host; 20 | proxy_set_header X-Real-IP $remote_addr; 21 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 22 | proxy_set_header X-Forwarded-Proto $scheme; 23 | } 24 | 25 | location /static/ { 26 | alias /home/app/web/project/static/; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | SPHINXOPTS ?= 2 | SPHINXBUILD ?= sphinx-build 3 | SOURCEDIR = source 4 | BUILDDIR = _build 5 | 6 | # Put it first so that "make" without argument is like "make help". 7 | help: 8 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 9 | 10 | .PHONY: help Makefile 11 | 12 | api-docs: 13 | sphinx-apidoc -o "$(SOURCEDIR)" .. ../*setup* ../*tests* 14 | 15 | # Catch-all target: route all unknown targets to Sphinx using the new 16 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 17 | %: Makefile 18 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 19 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx~=3.3.1 2 | sphinx-automodapi~=0.13 3 | sphinx_rtd_theme~=0.5.0 4 | sphinxcontrib-napoleon~=0.7 5 | sphinx-click~=2.5.0 6 | sphinxcontrib-httpdomain~=1.7.0 -------------------------------------------------------------------------------- /docs/source/_static/css/images/overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/docs/source/_static/css/images/overlay.png -------------------------------------------------------------------------------- /docs/source/_static/css/images/shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/docs/source/_static/css/images/shadow.png -------------------------------------------------------------------------------- /docs/source/_static/images/MegaQC_logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/docs/source/_static/images/MegaQC_logo.ai -------------------------------------------------------------------------------- /docs/source/_static/images/MegaQC_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/docs/source/_static/images/MegaQC_logo.png -------------------------------------------------------------------------------- /docs/source/_static/images/MegaQC_logo_darkbg.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/docs/source/_static/images/MegaQC_logo_darkbg.ai -------------------------------------------------------------------------------- /docs/source/_static/images/MegaQC_logo_darkbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/docs/source/_static/images/MegaQC_logo_darkbg.png -------------------------------------------------------------------------------- /docs/source/_static/images/header_bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/docs/source/_static/images/header_bg.jpg -------------------------------------------------------------------------------- /docs/source/_static/megaqc_style.css: -------------------------------------------------------------------------------- 1 | @import "basic.css"; 2 | 3 | /*Set max width to none so the theme uses all available width*/ 4 | .wy-nav-content { 5 | max-width: none; 6 | } 7 | -------------------------------------------------------------------------------- /docs/source/_templates/README.txt: -------------------------------------------------------------------------------- 1 | Dimension by HTML5 UP 2 | html5up.net | @ajlkn 3 | Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 4 | 5 | 6 | This is Dimension, a fun little one-pager with modal-ized (is that a word?) "pages" 7 | and a cool depth effect (click on a menu item to see what I mean). Simple, fully 8 | responsive, and kitted out with all the usual pre-styled elements you'd expect. 9 | Hope you dig it :) 10 | 11 | Demo images* courtesy of Unsplash, a radtastic collection of CC0 (public domain) images 12 | you can use for pretty much whatever. 13 | 14 | (* = not included) 15 | 16 | AJ 17 | aj@lkn.io | @ajlkn 18 | 19 | 20 | Credits: 21 | 22 | Demo Images: 23 | Unsplash (unsplash.com) 24 | 25 | Icons: 26 | Font Awesome (fontawesome.io) 27 | 28 | Other: 29 | jQuery (jquery.com) 30 | Responsive Tools (github.com/ajlkn/responsive-tools) -------------------------------------------------------------------------------- /docs/source/api_reference.rst: -------------------------------------------------------------------------------- 1 | .. _megaqc_api_reference: 2 | 3 | ========================== 4 | MegaQC API Reference 5 | ========================== 6 | 7 | Flask API Reference 8 | =========================== 9 | 10 | .. autoflask:: megaqc.app:create_app('megaqc.settings.ProdConfig') 11 | :undoc-static: 12 | 13 | 14 | Flask Quick API Reference 15 | ============================ 16 | 17 | 18 | .. qrefflask:: megaqc.app:create_app('megaqc.settings.ProdConfig') 19 | :undoc-static: 20 | 21 | .. https://sphinxcontrib-httpdomain.readthedocs.io/en/stable/#module-sphinxcontrib.autohttp.flask -------------------------------------------------------------------------------- /docs/source/cli.rst: -------------------------------------------------------------------------------- 1 | MegaQC Commandline Interface 2 | ================================ 3 | 4 | .. click:: megaqc.cli:cli 5 | :prog: megaqc 6 | :nested: full 7 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import megaqc 5 | 6 | basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) 7 | sys.path.insert(0, basedir) 8 | 9 | 10 | # -- Project information ----------------------------------------------------- 11 | 12 | project = "MegaQC" 13 | copyright = "2020, MegaQC Team" 14 | author = "MegaQC Team" 15 | 16 | version = megaqc.version 17 | 18 | release = megaqc.version 19 | 20 | 21 | # -- General configuration --------------------------------------------------- 22 | 23 | extensions = [ 24 | "sphinx.ext.autodoc", 25 | "sphinx.ext.autosummary", 26 | "sphinx.ext.doctest", 27 | "sphinx.ext.mathjax", 28 | "sphinx.ext.linkcode", # link to github, see linkcode_resolve() below 29 | "sphinxcontrib.napoleon", 30 | "sphinx_click", 31 | "sphinxcontrib.autohttp.flask", 32 | "sphinxcontrib.autohttp.flaskqref", 33 | ] 34 | 35 | # Replace the usual index.rst with a custom index.html file 36 | master_doc = "docs/index" 37 | html_additional_pages = {"index": "index.html"} 38 | 39 | templates_path = ["_templates"] 40 | 41 | # To prevent duplicate label warnings every *.rst file with multiple labels 42 | # needs to be added to the exclude patterns. 43 | # Reference: https://stackoverflow.com/questions/16262163/sphinxs-include-directive-and-duplicate-label-warnings 44 | exclude_patterns = [ 45 | "_build", 46 | "Thumbs.db", 47 | ".DS_Store", 48 | "docs/dev/backend.rst", 49 | "docs/dev/frontend.rst", 50 | "docs/installation/installation_dev.rst", 51 | "docs/installation/installation_docker.rst", 52 | "docs/installation/installation_prod.rst", 53 | "docs/installation/migrations.rst", 54 | "docs/usage/usage_admin.rst", 55 | "docs/usage/usage_setup.rst", 56 | ] 57 | 58 | html_theme = "sphinx_rtd_theme" 59 | 60 | html_static_path = ["_static"] 61 | html_css_files = [ 62 | "megaqc_style.css", 63 | ] 64 | 65 | html_context = { 66 | # Enable the "Edit in GitHub link within the header of each page. 67 | "display_github": True, 68 | # Set the following variables to generate the resulting github URL for each page. 69 | # Format Template: https://{{ github_host|default("github.com") }}/{{ github_user }}/{{ github_repo }}/blob/{{ github_version }}{{ conf_py_path }}{{ pagename }}{{ suffix }} 70 | "github_user": "MultiQC", 71 | "github_repo": "MegaQC", 72 | "github_version": "main/docs", 73 | "conf_py_path": "/source/", 74 | } 75 | 76 | 77 | # Resolve function for the linkcode extension. 78 | def linkcode_resolve(domain, info): 79 | def find_source(): 80 | # try to find the file and line number: 81 | obj = sys.modules[info["module"]] 82 | for part in info["fullname"].split("."): 83 | obj = getattr(obj, part) 84 | import inspect 85 | import os 86 | 87 | fn = inspect.getsourcefile(obj) 88 | fn = os.path.relpath(fn, start=os.path.dirname(megaqc.__file__)) 89 | source, lineno = inspect.getsourcelines(obj) 90 | return fn, lineno, lineno + len(source) - 1 91 | 92 | if domain != "py" or not info["module"]: 93 | return None 94 | try: 95 | filename = "megaqc/%s#L%d-L%d" % find_source() 96 | except Exception: 97 | filename = info["module"].replace(".", "/") + ".py" 98 | tag = "main" 99 | # TODO use this after the first release: tag = 'main' if 'dev' in release else ('v' + release) 100 | 101 | return "https://github.com/MultiQC/MegaQC/blob/%s/%s" % (tag, filename) 102 | -------------------------------------------------------------------------------- /docs/source/docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../../CHANGELOG.rst -------------------------------------------------------------------------------- /docs/source/docs/code_of_conduct.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../../../CODE_OF_CONDUCT.rst -------------------------------------------------------------------------------- /docs/source/docs/dev/backend.rst: -------------------------------------------------------------------------------- 1 | Backend 2 | ======= 3 | 4 | The Python backend is written in `flask`_, which determines the 5 | structure of the project. 6 | 7 | Database 8 | -------- 9 | 10 | MegaQC uses `SQLAlchemy`_ to handle database access, which means it can 11 | integrate with any SQL database that it supports. For development this 12 | will likely be SQLite. 13 | 14 | Database models are located in `model/models.py`_ and `user/models.py`_. 15 | 16 | Database schema migrations 17 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 18 | 19 | You need to generate a new migration whenever the database schema (ie 20 | any models class) changes. To generate a migration: 21 | 22 | .. code:: bash 23 | 24 | cd megaqc 25 | export FLASK_APP=wsgi.py 26 | flask db upgrade # Update to the latest migration 27 | flask db migrate 28 | 29 | API 30 | --- 31 | 32 | MegaQC actually has two APIs. The first, older API is accessed at 33 | ``/api``, and the code for this API is located in `megaqc/api`_. 34 | This API is implemented using regular flask views. 35 | 36 | However, all future development should be done on the newer REST API. 37 | This is accessed at ``/rest_api/v1``, and the code for it is located in 38 | `megaqc/rest_api`_. This API is composed of views, located in 39 | ``views.py``. These view classes, which rely on `flapison`_, each 40 | define an SQLAlchemy model that defines how to access the data, and a 41 | Marshmallow schema, which defines how to serialize and deserialize the 42 | data to JSON. The Marshmallow schemas themselves are defined in 43 | `schemas.py`_. For more information, refer to the flapison documentation. 44 | 45 | Views 46 | ----- 47 | 48 | Flask endpoints that return HTML (the non-API URLs) are defined in 49 | `public/views.py`_, and `user/views.py`_. All of these render a 50 | `Jinja2`_ template, which all the other frontend CSS and JavaScript is 51 | connected to. This is further explained in the `frontend`_ docs. 52 | 53 | Tests 54 | ----- 55 | 56 | Python tests are located in the `python_tests`_ folder. Please note that 57 | there are currently no Javascript tests. To run the Python tests, use 58 | ``pytest test``. Every new URL should be tested, although since new 59 | pages are likely to rely on React and the REST API, testing can mostly 60 | be done inside `test_api.py`_, which tests all REST API endpoints. 61 | 62 | .. _flask: https://www.palletsprojects.com/p/flask/ 63 | .. _SQLAlchemy: https://docs.sqlalchemy.org/ 64 | .. _model/models.py: https://github.com/MultiQC/MegaQC/blob/main/megaqc/model/models.py 65 | .. _user/models.py: https://github.com/MultiQC/MegaQC/blob/main/megaqc/user/models.py 66 | .. _megaqc/api: https://github.com/MultiQC/MegaQC/tree/main/megaqc/api 67 | .. _megaqc/rest_api: https://github.com/MultiQC/MegaQC/tree/main/megaqc/rest_api 68 | .. _views.py: https://github.com/MultiQC/MegaQC/blob/main/megaqc/rest_api/views.py 69 | .. _flapison: https://github.com/TMiguelT/flapison 70 | .. _schemas.py: https://github.com/MultiQC/MegaQC/tree/main/megaqc/rest_api/schemas.py 71 | .. _public/views.py: https://github.com/MultiQC/MegaQC/tree/main/megaqc/public/views.py 72 | .. _user/views.py: https://github.com/MultiQC/MegaQC/tree/main/megaqc/user/views.py 73 | .. _Jinja2: https://jinja.palletsprojects.com/en/2.11.x/ 74 | .. _frontend: ./frontend.md 75 | .. _python_tests: https://github.com/MultiQC/MegaQC/tree/main/tests 76 | .. _test_api.py: https://github.com/MultiQC/MegaQC/tree/main/megaqc/api/test_api.py 77 | -------------------------------------------------------------------------------- /docs/source/docs/dev/development.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | Development 3 | =========== 4 | 5 | The following sections are aimed at developers trying to contribute to MegaQC. 6 | 7 | .. TODO I would love to point people to a contributions guidelines section or something. 8 | .. Maybe this file here actually is just that and we should just rename it accordingly? :) 9 | 10 | Please follow the :ref:`installation_dev` guidelines to install a development ready version of MegaQC. 11 | You should also be aware of the detailed :ref:`megaqc_module` and :ref:`megaqc_api_reference`. 12 | Next, some technical details regarding the front and backend are explained. 13 | 14 | .. include:: backend.rst 15 | .. include:: frontend.rst 16 | .. include:: documentation.rst 17 | .. include:: ../../modules.rst 18 | -------------------------------------------------------------------------------- /docs/source/docs/dev/documentation.rst: -------------------------------------------------------------------------------- 1 | Documentation 2 | =============== 3 | 4 | MegaQC uses `Sphinx `_ to build the documentation 5 | and `Github Pages `_ to host it. 6 | 7 | Building the documentation locally 8 | ------------------------------------- 9 | 10 | The MegaQC documentation requires 11 | 12 | 1. An installation of MegaQC to fetch the API endpoints and the Click commands. 13 | 2. All dependencies specified in the `docs requirements.txt `_. 14 | Install them by invoking: ``pip install -r docs/requirements.txt``. 15 | 16 | After having installed all requirements run ``make api-docs && make html`` in the ``docs`` directory. 17 | The generated ``html`` files are found in the ``docs/_build/html`` subfolder. 18 | Simply open a generated ``html`` file in your favorite browser to read the documentation. 19 | 20 | Publishing the documentation 21 | --------------------------------- 22 | 23 | On pushes to the ``main`` branch, the documentation is automatically built and pushed 24 | to the ``gh-pages`` branch. The static html files on this branch are then deployed 25 | to Github Pages and displayed to the outside world. 26 | All of this is done with the `Publish Docs Github Actions workflow `. 27 | -------------------------------------------------------------------------------- /docs/source/docs/dev/frontend.rst: -------------------------------------------------------------------------------- 1 | Frontend 2 | ======== 3 | 4 | As with the API, MegaQC is currently transitioning from using an HTML + 5 | CSS + JavaScript frontend to `React`_. This can be confusing, as a 6 | number of technologies are mixed in the same project. 7 | 8 | As explained in the backends section, each URL in MegaQC renders a 9 | Jinja2 template. The old-style endpoints have a lot of HTML code in 10 | their `templates`_ that defines the entire page layout, and load 11 | JavaScript and CSS located in the `static`_ directory. 12 | 13 | However, the newer React pages instead render a special template, called 14 | `react.html`_, which is a very simple page that acts as the entry 15 | point for the React code, which handles all layout and logic. 16 | 17 | The source code for the React JavaScript can be found in the `src`_ 18 | directory, which currently has one root-level ``.js`` file for each 19 | React page that then imports other components to use. 20 | 21 | Note that all new pages going forward should be written using React, to 22 | improve the maintainability of the frontend. 23 | 24 | .. _React: https://reactjs.org/ 25 | .. _templates: https://github.com/MultiQC/MegaQC/tree/main/megaqc/templates 26 | .. _static: https://github.com/MultiQC/MegaQC/tree/main/megaqc/static 27 | .. _react.html: https://github.com/MultiQC/MegaQC/tree/main/megaqc/templates/public/react.html 28 | .. _src: https://github.com/MultiQC/MegaQC/tree/main/src 29 | -------------------------------------------------------------------------------- /docs/source/docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to MegaQC's documentation! 2 | ================================== 3 | 4 | |MegaQC| 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | :caption: Contents: 9 | 10 | installation/installation 11 | usage/usage 12 | dev/development 13 | ../api_reference 14 | troubleshooting 15 | changelog 16 | code_of_conduct 17 | 18 | 19 | Indices and tables 20 | ================== 21 | 22 | * :ref:`genindex` 23 | * :ref:`modindex` 24 | * :ref:`search` 25 | 26 | .. |MegaQC| image:: https://raw.githubusercontent.com/MultiQC/MegaQC/main/megaqc/static/img/MegaQC_logo.png 27 | -------------------------------------------------------------------------------- /docs/source/docs/installation/installation.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Installation 3 | =============== 4 | 5 | MegaQC has been written in Python using the `Flask`_ web framework. 6 | MegaQC is designed to be very simple to get up and running for basic 7 | testing and evaluation, yet super easy to configure for a high 8 | performance production installation. The various ways of getting a runnable 9 | MegaQC instance are explained in the following sections. 10 | 11 | .. include:: installation_prod.rst 12 | .. _docker_installation: 13 | .. include:: installation_docker.rst 14 | .. _installation_dev: 15 | .. include:: installation_dev.rst 16 | .. include:: migrations.rst 17 | 18 | .. _Flask: http://flask.pocoo.org 19 | -------------------------------------------------------------------------------- /docs/source/docs/installation/migrations.rst: -------------------------------------------------------------------------------- 1 | Migrations 2 | ========== 3 | 4 | Introduction 5 | ------------ 6 | 7 | Migrations are updates to a database schema. This is relevant if, for 8 | example, you set up a MegaQC database (using ``initdb``), and then a new 9 | version of MegaQC is released that needs new tables or columns. 10 | 11 | When to migrate 12 | --------------- 13 | 14 | Every time a new version of MegaQC is released, you should ensure your 15 | database is up to date. You don’t need to run the migrations the first 16 | time you install MegaQC, because the ``megaqc initdb`` command replaces 17 | the need for migrations. 18 | 19 | How to migrate 20 | -------------- 21 | 22 | To migrate, run the following commands: 23 | 24 | .. code:: bash 25 | 26 | cd megaqc 27 | export FLASK_APP=wsgi.py 28 | flask db upgrade 29 | 30 | Note: when you run these migrations, you **must** have the same 31 | environment as you use to run MegaQC normally, which means the same 32 | value of ``FLASK_DEBUG`` and ``MEGAQC_PRODUCTION`` environment 33 | variables. Otherwise it will migrate the wrong database 34 | (or a non-existing one). 35 | 36 | Stamping your database 37 | ---------------------- 38 | 39 | The complete migration history has only recently been added. This means 40 | that, if you were using MegaQC in the past when migrations were not 41 | included in the repo, your database won’t know what version you’re currently at. 42 | 43 | To fix this, first you need to work out which migration your database is 44 | up to. Browse through the files in ``megaqc/migrations/versions``, 45 | starting from the oldest date (at the top of each file), until you find 46 | a change that wasn’t present in your database. At this point, note the 47 | ``revision`` value at the top of the file, (e.g. ``revision = "007c354223ec"``). 48 | 49 | Next, run the following command, replacing ```` with the 50 | revision you noted above: 51 | 52 | .. code:: bash 53 | 54 | flask db stamp 55 | -------------------------------------------------------------------------------- /docs/source/docs/troubleshooting.rst: -------------------------------------------------------------------------------- 1 | Troubleshooting 2 | =============== 3 | 4 | Installation/setup 5 | -------------------- 6 | 7 | - I’m getting a blank page when I open the ``/trends`` or ``/admin`` pages 8 | 9 | - Make sure you’ve compiled the JavaScript correctly. 10 | - Ensure you’ve run ``npm install`` and then ``npm run build`` or ``npm run watch``. 11 | 12 | - I am getting ``sqlalchemy.exc.OperationalError: (psycopg2.OperationalError) could not connect to server: Connection refused Is the server running on host "127.0.0.1" and accepting TCP/IP connections on port 5432?`` 13 | 14 | - Please ensure that you have a Postgres database accessible at "127.0.0.1:5432". 15 | 16 | Usage 17 | --------- 18 | 19 | - When creating a violin plot I get an error. 20 | 21 | - This is a known issue: https://github.com/MultiQC/MegaQC/issues/31 22 | 23 | - TypeError: '<' not supported between instances of 'NoneType' and 'float' when trying to compare data 24 | 25 | - This is a known issue: https://github.com/MultiQC/MegaQC/issues/156 26 | - Please ensure that your reports do not have missing fields 27 | -------------------------------------------------------------------------------- /docs/source/docs/usage/usage.rst: -------------------------------------------------------------------------------- 1 | ===== 2 | Usage 3 | ===== 4 | 5 | In the following sections general details regarding the usage 6 | and administation of MegaQC are provided. 7 | 8 | .. _cli_doc: 9 | .. include:: ../../cli.rst 10 | .. include:: usage_setup.rst 11 | .. include:: usage_admin.rst 12 | -------------------------------------------------------------------------------- /docs/source/docs/usage/usage_admin.rst: -------------------------------------------------------------------------------- 1 | MegaQC Usage: Administration 2 | ============================ 3 | 4 | Initial setup 5 | ------------- 6 | 7 | Once you have your MegaQC server up and running, load up the address in 8 | your web browser. The MegaQC installation creates an empty database 9 | without any registered users. The first user to register will automatically 10 | be an administrator, so make sure that you immediately register! 11 | 12 | User administration 13 | ------------------- 14 | 15 | Admins have access to a page in the navigation called *Administration 16 | Panel* where you can create, delete and reset passwords for other MegaQC users. 17 | -------------------------------------------------------------------------------- /docs/source/docs/usage/usage_setup.rst: -------------------------------------------------------------------------------- 1 | MegaQC Usage: Setup 2 | =================== 3 | 4 | Submitting data 5 | --------------- 6 | 7 | Before you can do anything useful in MegaQC, you need to submit some 8 | data to the database. You do this by configuring and then running MultiQC. 9 | 10 | MultiQC configuration 11 | ~~~~~~~~~~~~~~~~~~~~~ 12 | 13 | MultiQC needs a couple of configuration variables to know how to send 14 | data to MegaQC. To find these, log into MegaQC and use the navigation 15 | dropdown to get to the *MultiQC Configuration* page. Copy the specified 16 | text into ``~/.multiqc_config.yaml``. 17 | 18 | Note that this ``megaqc_access_token`` is specific to your MegaQC user, 19 | so shouldn’t be shared (it’s effectively a password). All data uploaded 20 | using that token will be attributed to your user. 21 | 22 | Running MultiQC 23 | ~~~~~~~~~~~~~~~ 24 | 25 | Once configured, run MultiQC as you would normally. You should see a 26 | couple of additional log messages under the ``megaqc`` namespace as 27 | follows: 28 | 29 | :: 30 | 31 | $ multiqc . 32 | 33 | [INFO ] multiqc : This is MultiQC v1.3 34 | [INFO ] multiqc : Template : default 35 | [INFO ] multiqc : Searching './' 36 | Searching 63 files.. [####################################] 100% 37 | [INFO ] feature_counts : Found 6 reports 38 | [INFO ] star : Found 6 reports 39 | [INFO ] cutadapt : Found 6 reports 40 | [INFO ] fastq_screen : Found 6 reports 41 | [INFO ] fastqc : Found 6 reports 42 | [INFO ] multiqc : Compressing plot data 43 | [INFO ] megaqc : Sending data to MegaQC 44 | [INFO ] megaqc : Data upload successful 45 | [INFO ] multiqc : Report : multiqc_report.html 46 | [INFO ] multiqc : Data : multiqc_data 47 | [INFO ] multiqc : MultiQC complete 48 | 49 | **NB: You need MultiQC v1.3 or later for MegaQC integration to work.** 50 | -------------------------------------------------------------------------------- /docs/source/images/megaqc_homepage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/docs/source/images/megaqc_homepage.png -------------------------------------------------------------------------------- /docs/source/megaqc.api.rst: -------------------------------------------------------------------------------- 1 | megaqc.api package 2 | ================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | megaqc.api.constants module 8 | --------------------------- 9 | 10 | .. automodule:: megaqc.api.constants 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | megaqc.api.utils module 16 | ----------------------- 17 | 18 | .. automodule:: megaqc.api.utils 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | megaqc.api.views module 24 | ----------------------- 25 | 26 | .. automodule:: megaqc.api.views 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | 32 | Module contents 33 | --------------- 34 | 35 | .. automodule:: megaqc.api 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | -------------------------------------------------------------------------------- /docs/source/megaqc.model.rst: -------------------------------------------------------------------------------- 1 | megaqc.model package 2 | ==================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | megaqc.model.models module 8 | -------------------------- 9 | 10 | .. automodule:: megaqc.model.models 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: megaqc.model 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/source/megaqc.public.rst: -------------------------------------------------------------------------------- 1 | megaqc.public package 2 | ===================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | megaqc.public.forms module 8 | -------------------------- 9 | 10 | .. automodule:: megaqc.public.forms 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | megaqc.public.views module 16 | -------------------------- 17 | 18 | .. automodule:: megaqc.public.views 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | 24 | Module contents 25 | --------------- 26 | 27 | .. automodule:: megaqc.public 28 | :members: 29 | :undoc-members: 30 | :show-inheritance: 31 | -------------------------------------------------------------------------------- /docs/source/megaqc.report_plot.rst: -------------------------------------------------------------------------------- 1 | megaqc.report\_plot package 2 | =========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | megaqc.report\_plot.views module 8 | -------------------------------- 9 | 10 | .. automodule:: megaqc.report_plot.views 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: megaqc.report_plot 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/source/megaqc.rest_api.rst: -------------------------------------------------------------------------------- 1 | megaqc.rest\_api package 2 | ======================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | megaqc.rest\_api.content module 8 | ------------------------------- 9 | 10 | .. automodule:: megaqc.rest_api.content 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | megaqc.rest\_api.fields module 16 | ------------------------------ 17 | 18 | .. automodule:: megaqc.rest_api.fields 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | megaqc.rest\_api.filters module 24 | ------------------------------- 25 | 26 | .. automodule:: megaqc.rest_api.filters 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | megaqc.rest\_api.outlier module 32 | ------------------------------- 33 | 34 | .. automodule:: megaqc.rest_api.outlier 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | megaqc.rest\_api.plot module 40 | ---------------------------- 41 | 42 | .. automodule:: megaqc.rest_api.plot 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | megaqc.rest\_api.schemas module 48 | ------------------------------- 49 | 50 | .. automodule:: megaqc.rest_api.schemas 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | megaqc.rest\_api.utils module 56 | ----------------------------- 57 | 58 | .. automodule:: megaqc.rest_api.utils 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | megaqc.rest\_api.views module 64 | ----------------------------- 65 | 66 | .. automodule:: megaqc.rest_api.views 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | megaqc.rest\_api.webarg\_parser module 72 | -------------------------------------- 73 | 74 | .. automodule:: megaqc.rest_api.webarg_parser 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | 79 | 80 | Module contents 81 | --------------- 82 | 83 | .. automodule:: megaqc.rest_api 84 | :members: 85 | :undoc-members: 86 | :show-inheritance: 87 | -------------------------------------------------------------------------------- /docs/source/megaqc.rst: -------------------------------------------------------------------------------- 1 | .. _megaqc_module: 2 | 3 | megaqc package 4 | ============== 5 | 6 | Subpackages 7 | ----------- 8 | 9 | .. toctree:: 10 | 11 | megaqc.api 12 | megaqc.model 13 | megaqc.public 14 | megaqc.report_plot 15 | megaqc.rest_api 16 | megaqc.user 17 | megaqc.utils 18 | 19 | Submodules 20 | ---------- 21 | 22 | megaqc.app module 23 | ----------------- 24 | 25 | .. automodule:: megaqc.app 26 | :members: 27 | :undoc-members: 28 | :show-inheritance: 29 | 30 | megaqc.cli module 31 | ----------------- 32 | 33 | .. automodule:: megaqc.cli 34 | :members: 35 | :undoc-members: 36 | :show-inheritance: 37 | 38 | megaqc.commands module 39 | ---------------------- 40 | 41 | .. automodule:: megaqc.commands 42 | :members: 43 | :undoc-members: 44 | :show-inheritance: 45 | 46 | megaqc.compat module 47 | -------------------- 48 | 49 | .. automodule:: megaqc.compat 50 | :members: 51 | :undoc-members: 52 | :show-inheritance: 53 | 54 | megaqc.database module 55 | ---------------------- 56 | 57 | .. automodule:: megaqc.database 58 | :members: 59 | :undoc-members: 60 | :show-inheritance: 61 | 62 | megaqc.extensions module 63 | ------------------------ 64 | 65 | .. automodule:: megaqc.extensions 66 | :members: 67 | :undoc-members: 68 | :show-inheritance: 69 | 70 | megaqc.scheduler module 71 | ----------------------- 72 | 73 | .. automodule:: megaqc.scheduler 74 | :members: 75 | :undoc-members: 76 | :show-inheritance: 77 | 78 | megaqc.settings module 79 | ---------------------- 80 | 81 | .. automodule:: megaqc.settings 82 | :members: 83 | :undoc-members: 84 | :show-inheritance: 85 | 86 | megaqc.wsgi module 87 | ------------------ 88 | 89 | .. automodule:: megaqc.wsgi 90 | :members: 91 | :undoc-members: 92 | :show-inheritance: 93 | 94 | 95 | Module contents 96 | --------------- 97 | 98 | .. automodule:: megaqc 99 | :members: 100 | :undoc-members: 101 | :show-inheritance: 102 | -------------------------------------------------------------------------------- /docs/source/megaqc.user.rst: -------------------------------------------------------------------------------- 1 | megaqc.user package 2 | =================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | megaqc.user.forms module 8 | ------------------------ 9 | 10 | .. automodule:: megaqc.user.forms 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | megaqc.user.models module 16 | ------------------------- 17 | 18 | .. automodule:: megaqc.user.models 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | megaqc.user.views module 24 | ------------------------ 25 | 26 | .. automodule:: megaqc.user.views 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | 32 | Module contents 33 | --------------- 34 | 35 | .. automodule:: megaqc.user 36 | :members: 37 | :undoc-members: 38 | :show-inheritance: 39 | -------------------------------------------------------------------------------- /docs/source/megaqc.utils.rst: -------------------------------------------------------------------------------- 1 | megaqc.utils package 2 | ==================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | megaqc.utils.settings module 8 | ---------------------------- 9 | 10 | .. automodule:: megaqc.utils.settings 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | 16 | Module contents 17 | --------------- 18 | 19 | .. automodule:: megaqc.utils 20 | :members: 21 | :undoc-members: 22 | :show-inheritance: 23 | -------------------------------------------------------------------------------- /docs/source/modules.rst: -------------------------------------------------------------------------------- 1 | MegaQC Modules Overview 2 | ========================== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | ../../megaqc 8 | -------------------------------------------------------------------------------- /logo/MegaQC_logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/logo/MegaQC_logo.ai -------------------------------------------------------------------------------- /logo/MegaQC_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/logo/MegaQC_logo.png -------------------------------------------------------------------------------- /logo/MegaQC_logo_darkbg.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/logo/MegaQC_logo_darkbg.ai -------------------------------------------------------------------------------- /logo/MegaQC_logo_darkbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/logo/MegaQC_logo_darkbg.png -------------------------------------------------------------------------------- /megaqc/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Main application package. 4 | """ 5 | import pkg_resources 6 | 7 | version = pkg_resources.get_distribution("megaqc").version 8 | -------------------------------------------------------------------------------- /megaqc/api/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | The user module. 4 | """ 5 | from . import views # noqa 6 | -------------------------------------------------------------------------------- /megaqc/api/constants.py: -------------------------------------------------------------------------------- 1 | import re 2 | from collections import OrderedDict 3 | 4 | from megaqc.model.models import * 5 | 6 | custom_plots_re = re.compile("^mqc_hcplot_[a-z]{10}$") 7 | 8 | valid_join_conditions = { 9 | Sample: { 10 | Report: ("report_id", "report_id"), 11 | SampleData: ("sample_id", "sample_id"), 12 | }, 13 | Report: { 14 | ReportMeta: ("report_id", "report_id"), 15 | Sample: ("report_id", "report_id"), 16 | }, 17 | PlotData: {Sample: ("report_id", "report_id")}, 18 | SampleData: { 19 | SampleDataType: ("sample_data_type_id", "sample_data_type_id"), 20 | Sample: ("report_id", "report_id"), 21 | }, 22 | } 23 | type_to_tables_fields = { 24 | "daterange": {Report: ["created_at", "created_at"]}, 25 | "timedelta": {Report: ["created_at"]}, 26 | "samplenames": {Sample: ["sample_name"]}, 27 | "sampleids": {Sample: ["sample_id"]}, 28 | "reportmeta": OrderedDict( 29 | [(Report, []), (ReportMeta, ["report_meta_value", "report_meta_key"])] 30 | ), 31 | "samplemeta": OrderedDict( 32 | [(SampleData, ["value"]), (SampleDataType, ["data_key", "data_section"])] 33 | ), 34 | "samplemetaids": OrderedDict([(SampleDataType, ["sample_data_type_id"])]), 35 | } 36 | comparators = { 37 | "gt": "__gt__", 38 | ">": "__gt__", 39 | "<": "__lt__", 40 | "lt": "__lt__", 41 | "ge": "__ge__", 42 | ">=": "__ge__", 43 | "<=": "__le__", 44 | "le": "__le__", 45 | "eq": "__eq__", 46 | "==": "__eq__", 47 | "=": "__eq__", 48 | "in": "contains", 49 | "inlist": "in_", 50 | "ne": "__ne__", 51 | "!=": "__ne__", 52 | "not in": "notlike", 53 | } 54 | -------------------------------------------------------------------------------- /megaqc/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | MegaQC: a web application that collects results from multiple runs of MultiQC and allows 5 | bulk visualisation. 6 | """ 7 | 8 | import os 9 | import sys 10 | 11 | import click 12 | import pkg_resources 13 | import sqlalchemy 14 | from environs import Env 15 | from flask.cli import FlaskGroup 16 | 17 | from megaqc import settings 18 | 19 | env = Env() 20 | 21 | 22 | def create_megaqc_app(): 23 | from megaqc.app import create_app 24 | from megaqc.settings import DevConfig, ProdConfig, TestConfig 25 | 26 | if env.bool("FLASK_DEBUG", False): 27 | CONFIG = DevConfig() 28 | elif env.bool("MEGAQC_PRODUCTION", False): 29 | CONFIG = ProdConfig() 30 | else: 31 | CONFIG = TestConfig() 32 | 33 | if settings.run_db_check: 34 | # Attempt to connect to the database exists to check that it exists 35 | dbengine = sqlalchemy.create_engine(CONFIG.SQLALCHEMY_DATABASE_URI).connect() 36 | metadata = sqlalchemy.MetaData(dbengine) 37 | metadata.reflect(dbengine) 38 | if "sample_data" not in metadata.tables: 39 | print("\n##### ERROR! Could not find table 'sample_data' in database!") 40 | print( 41 | "Has the database been initialised? If not, please run 'megaqc initdb' first" 42 | ) 43 | print("Exiting...\n") 44 | sys.exit(1) 45 | else: 46 | dbengine.close() 47 | 48 | return create_app(CONFIG) 49 | 50 | 51 | @click.group(cls=FlaskGroup, create_app=create_megaqc_app) 52 | @click.pass_context 53 | def cli(ctx): 54 | """ 55 | Welcome to the MegaQC command line interface. 56 | 57 | \nSee below for the available commands - for example, 58 | to start the MegaQC server, use the command: megaqc run 59 | """ 60 | # If the invoked command is not initdb we need to check whether a database already exists 61 | if ctx.invoked_subcommand != "initdb": 62 | settings.run_db_check = True 63 | 64 | 65 | def main(): 66 | version = pkg_resources.get_distribution("megaqc").version 67 | print("This is MegaQC v{}\n".format(version)) 68 | 69 | if env.bool("FLASK_DEBUG", False): 70 | print(" * Environment variable FLASK_DEBUG is true - running in dev mode") 71 | os.environ["FLASK_ENV"] = "dev" 72 | elif not env.bool("MEGAQC_PRODUCTION", False): 73 | os.environ["FLASK_ENV"] = "test" 74 | cli() 75 | 76 | 77 | if __name__ == "__main__": 78 | main() 79 | -------------------------------------------------------------------------------- /megaqc/compat.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python 2/3 compatibility module. 4 | """ 5 | import sys 6 | from builtins import bytes 7 | 8 | PY2 = int(sys.version[0]) == 2 9 | 10 | if PY2: 11 | text_type = unicode # noqa 12 | binary_type = str 13 | string_types = (str, unicode) # noqa 14 | unicode = unicode # noqa 15 | basestring = basestring # noqa 16 | else: 17 | text_type = str 18 | binary_type = bytes 19 | string_types = (str,) 20 | unicode = str 21 | basestring = (str, bytes) 22 | -------------------------------------------------------------------------------- /megaqc/extensions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Extensions module. 4 | 5 | Each extension is initialized in the app factory located in app.py. 6 | """ 7 | from pathlib import Path 8 | 9 | from flask_caching import Cache 10 | from flask_debugtoolbar import DebugToolbarExtension 11 | from flask_login import LoginManager 12 | from flask_marshmallow import Marshmallow 13 | from flask_migrate import Migrate 14 | from flask_rest_jsonapi import Api as JsonApi 15 | from flask_restful import Api 16 | from flask_sqlalchemy import SQLAlchemy 17 | from flask_wtf.csrf import CSRFProtect 18 | 19 | csrf_protect = CSRFProtect() 20 | login_manager = LoginManager() 21 | db = SQLAlchemy() 22 | ma = Marshmallow() 23 | cache = Cache() 24 | debug_toolbar = DebugToolbarExtension() 25 | restful = Api(prefix="/rest_api/v1") 26 | migrate = Migrate(directory=str(Path(__file__).parent / "migrations")) 27 | json_api = JsonApi() 28 | -------------------------------------------------------------------------------- /megaqc/migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. 2 | -------------------------------------------------------------------------------- /megaqc/migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [handler_console] 38 | class = StreamHandler 39 | args = (sys.stderr,) 40 | level = NOTSET 41 | formatter = generic 42 | 43 | [formatter_generic] 44 | format = %(levelname)-5.5s [%(name)s] %(message)s 45 | datefmt = %H:%M:%S 46 | -------------------------------------------------------------------------------- /megaqc/migrations/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | 3 | import logging 4 | from logging.config import fileConfig 5 | 6 | from alembic import context 7 | from flask import current_app 8 | from sqlalchemy import engine_from_config, pool 9 | 10 | # this is the Alembic Config object, which provides 11 | # access to the values within the .ini file in use. 12 | config = context.config 13 | 14 | # Interpret the config file for Python logging. 15 | # This line sets up loggers basically. 16 | fileConfig(config.config_file_name) 17 | logger = logging.getLogger("alembic.env") 18 | 19 | config.set_main_option( 20 | "sqlalchemy.url", 21 | current_app.config.get("SQLALCHEMY_DATABASE_URI").replace("%", "%%"), 22 | ) 23 | target_metadata = current_app.extensions["migrate"].db.metadata 24 | 25 | # other values from the config, defined by the needs of env.py, 26 | # can be acquired: 27 | # my_important_option = config.get_main_option("my_important_option") 28 | # ... etc. 29 | 30 | 31 | def run_migrations_offline(): 32 | """ 33 | Run migrations in 'offline' mode. 34 | 35 | This configures the context with just a URL and not an Engine, though an Engine is 36 | acceptable here as well. By skipping the Engine creation we don't even need a DBAPI 37 | to be available. 38 | 39 | Calls to context.execute() here emit the given string to the script output. 40 | """ 41 | url = config.get_main_option("sqlalchemy.url") 42 | context.configure( 43 | url=url, target_metadata=target_metadata, literal_binds=True, compare_type=True 44 | ) 45 | 46 | with context.begin_transaction(): 47 | context.run_migrations() 48 | 49 | 50 | def run_migrations_online(): 51 | """ 52 | Run migrations in 'online' mode. 53 | 54 | In this scenario we need to create an Engine and associate a connection with the 55 | context. 56 | """ 57 | 58 | # this callback is used to prevent an auto-migration from being generated 59 | # when there are no changes to the schema 60 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 61 | def process_revision_directives(context, revision, directives): 62 | if getattr(config.cmd_opts, "autogenerate", False): 63 | script = directives[0] 64 | if script.upgrade_ops.is_empty(): 65 | directives[:] = [] 66 | logger.info("No changes in schema detected.") 67 | 68 | connectable = engine_from_config( 69 | config.get_section(config.config_ini_section), 70 | prefix="sqlalchemy.", 71 | poolclass=pool.NullPool, 72 | ) 73 | 74 | with connectable.connect() as connection: 75 | context.configure( 76 | connection=connection, 77 | compare_type=True, 78 | target_metadata=target_metadata, 79 | process_revision_directives=process_revision_directives, 80 | **current_app.extensions["migrate"].configure_args 81 | ) 82 | 83 | with context.begin_transaction(): 84 | context.run_migrations() 85 | 86 | 87 | if context.is_offline_mode(): 88 | run_migrations_offline() 89 | else: 90 | run_migrations_online() 91 | -------------------------------------------------------------------------------- /megaqc/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /megaqc/model/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from . import models 3 | -------------------------------------------------------------------------------- /megaqc/public/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | The public module, including the homepage and user auth. 4 | """ 5 | from . import views # noqa 6 | -------------------------------------------------------------------------------- /megaqc/public/forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Public forms. 4 | """ 5 | from flask_wtf import Form 6 | from wtforms import PasswordField, StringField 7 | from wtforms.validators import DataRequired 8 | 9 | from megaqc.user.models import User 10 | 11 | 12 | class LoginForm(Form): 13 | """ 14 | Login form. 15 | """ 16 | 17 | username = StringField("Username", validators=[DataRequired()]) 18 | password = PasswordField("Password", validators=[DataRequired()]) 19 | 20 | def __init__(self, *args, **kwargs): 21 | """ 22 | Create instance. 23 | """ 24 | super(LoginForm, self).__init__(*args, **kwargs) 25 | self.user = None 26 | 27 | def validate(self): 28 | """ 29 | Validate the form. 30 | """ 31 | initial_validation = super(LoginForm, self).validate() 32 | if not initial_validation: 33 | return False 34 | 35 | self.user = User.query.filter_by(username=self.username.data).first() 36 | if not self.user: 37 | self.username.errors.append("Unknown username") 38 | return False 39 | 40 | if not self.user.check_password(self.password.data): 41 | self.password.errors.append("Invalid password") 42 | return False 43 | 44 | if not self.user.active: 45 | self.username.errors.append("User not activated") 46 | return False 47 | return True 48 | -------------------------------------------------------------------------------- /megaqc/report_plot/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | The report_plot module, for all pages related to replicating plots from MultiQC reports. 4 | """ 5 | from . import views # noqa 6 | -------------------------------------------------------------------------------- /megaqc/report_plot/views.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/report_plot/views.py -------------------------------------------------------------------------------- /megaqc/rest_api/__init__.py: -------------------------------------------------------------------------------- 1 | from . import filters, schemas, utils, views 2 | -------------------------------------------------------------------------------- /megaqc/rest_api/content.py: -------------------------------------------------------------------------------- 1 | """ 2 | Utilities to help with content negotiation. 3 | """ 4 | import csv 5 | from io import StringIO 6 | 7 | 8 | def flatten_dicts(dictionary, delim=".", _path=None): 9 | """ 10 | Flattens a nested JSON dictionary into a flat dictionary, but does NOT flatten any 11 | lists in the structure. 12 | 13 | :param dictionary: Dictionary to flatten 14 | :param delim: The delimiter for nested fields 15 | """ 16 | flattened = {} 17 | prefix = _path + delim if _path else "" 18 | for key, value in dictionary.items(): 19 | if isinstance(value, dict): 20 | flattened.update(flatten_dicts(value, _path=prefix + key)) 21 | 22 | else: 23 | flattened[prefix + key] = value 24 | 25 | return flattened 26 | 27 | 28 | def json_to_csv(json, **writer_opts): 29 | # CSVs with one item are treated as lists of 1 element 30 | if not isinstance(json, list): 31 | json = [json] 32 | 33 | # Calculate the fieldnames 34 | fields = set() 35 | for row in json: 36 | flattened = flatten_dicts(row, delim="\t") 37 | # flattened = flatten_preserve_lists(row, max_depth=2) 38 | fields.update(flattened.keys()) 39 | 40 | # Write the CSV 41 | with StringIO() as fp: 42 | writer = csv.DictWriter(fp, fieldnames=fields, **writer_opts) 43 | writer.writeheader() 44 | for row in json: 45 | flattened = flatten_dicts(row, delim="\t") 46 | writer.writerow(flattened) 47 | 48 | return fp.getvalue() 49 | -------------------------------------------------------------------------------- /megaqc/rest_api/fields.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from flask_restful import url_for 4 | from sqlalchemy.inspection import inspect 5 | from sqlalchemy.orm.collections import InstrumentedList 6 | 7 | from megaqc.extensions import db, ma 8 | from megaqc.model import models 9 | 10 | 11 | class JsonString(ma.Field): 12 | """ 13 | Serializes a JSON structure as JSON, but deserializes it as a string (for DB 14 | storage), or vice-versa. 15 | """ 16 | 17 | def _jsonschema_type_mapping(self): 18 | return { 19 | "type": "string", 20 | } 21 | 22 | def __init__(self, *args, invert=False, **kwargs): 23 | self.invert = invert 24 | super().__init__(*args, **kwargs) 25 | 26 | def _serialize(self, value, attr, obj, **kwargs): 27 | if self.invert: 28 | return json.dumps(value) 29 | else: 30 | return json.loads(value) 31 | 32 | def _deserialize(self, value, attr, data, **kwargs): 33 | if self.invert: 34 | return json.loads(value) 35 | else: 36 | return json.dumps(value) 37 | 38 | 39 | class ModelAssociation(ma.Field): 40 | """ 41 | Dumps as a foreign key, e.g. "3", and loads as a model instance, e.g. User. 42 | """ 43 | 44 | def __init__(self, model, *args, **kwargs): 45 | super().__init__(*args, **kwargs) 46 | self.model = model 47 | 48 | def _serialize(self, value, attr, obj, **kwargs): 49 | return inspect(value).identity 50 | 51 | def _deserialize(self, value, attr, data, **kwargs): 52 | if not value: 53 | return None 54 | 55 | return db.session.query(self.model).get(value) 56 | 57 | 58 | class FilterReference(ModelAssociation): 59 | """ 60 | Dumps as a SampleFilter foreign key, e.g. "3", and loads as a filter array. 61 | """ 62 | 63 | def _jsonschema_type_mapping(self): 64 | return { 65 | "type": "array", 66 | } 67 | 68 | def __init__(self, *args, **kwargs): 69 | super().__init__(models.SampleFilter, *args, **kwargs) 70 | 71 | def _deserialize(self, value, attr, data, **kwargs): 72 | if not value: 73 | return [] 74 | 75 | instance = super()._deserialize(value, attr, data, **kwargs) 76 | if not instance: 77 | return [] 78 | 79 | return instance.filter_json 80 | -------------------------------------------------------------------------------- /megaqc/rest_api/outlier.py: -------------------------------------------------------------------------------- 1 | from numpy import absolute, delete, take, zeros 2 | from outliers import smirnov_grubbs as grubbs 3 | from scipy.stats import zscore 4 | 5 | 6 | class OutlierDetector: 7 | def __init__(self, threshold=None): 8 | self.threshold = threshold 9 | 10 | def get_outliers(self, y): 11 | """ 12 | Returns a boolean "mask array" that can be used to select outliers. 13 | """ 14 | 15 | # This default implementation returns an array of 0s 16 | return zeros(len(y), dtype=bool) 17 | 18 | 19 | class GrubbsDetector(OutlierDetector): 20 | def get_outliers(self, y): 21 | outlier_indices = grubbs.two_sided_test_indices(y, alpha=self.threshold) 22 | mask = zeros(len(y), dtype=bool) 23 | mask[outlier_indices] = 1 24 | return mask 25 | 26 | 27 | class ZScoreDetector(OutlierDetector): 28 | def get_outliers(self, y): 29 | return absolute(zscore(y)) > self.threshold 30 | -------------------------------------------------------------------------------- /megaqc/rest_api/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from enum import IntEnum, auto 3 | from functools import wraps 4 | from uuid import uuid4 5 | 6 | from flapison.exceptions import JsonApiException 7 | from flask import abort, request 8 | from flask.globals import current_app 9 | 10 | from megaqc.user.models import User 11 | 12 | 13 | def get_upload_dir(): 14 | upload_dir = current_app.config["UPLOAD_FOLDER"] 15 | if not os.path.isdir(upload_dir): 16 | os.mkdir(upload_dir) 17 | 18 | return upload_dir 19 | 20 | 21 | def get_unique_filename(): 22 | dir = get_upload_dir() 23 | while True: 24 | proposed = os.path.join(dir, str(uuid4())) 25 | if not os.path.exists(proposed): 26 | return proposed 27 | 28 | 29 | class Permission(IntEnum): 30 | NONUSER = auto() 31 | USER = auto() 32 | ADMIN = auto() 33 | 34 | 35 | def api_perms(min_level: Permission = Permission.NONUSER): 36 | """ 37 | Adds a "user" and "permission" kwarg to the view function. Also verifies a minimum 38 | permissions level. 39 | 40 | :param min_level: If provided, this is the minimum permission level 41 | required by this endpoint 42 | """ 43 | 44 | def wrapper(function): 45 | @wraps(function) 46 | def user_wrap_function(*args, **kwargs): 47 | extra = None 48 | if not request.headers.has_key("access_token"): 49 | perms = Permission.NONUSER 50 | user = None 51 | extra = "No access token provided. Please add a header with the name 'access_token'." 52 | else: 53 | user = User.query.filter_by( 54 | api_token=request.headers.get("access_token") 55 | ).first() 56 | if not user: 57 | perms = Permission.NONUSER 58 | extra = "The provided access token was invalid." 59 | elif user.is_anonymous: 60 | perms = Permission.NONUSER 61 | elif user.is_admin: 62 | perms = Permission.ADMIN 63 | elif not user.is_active(): 64 | perms = Permission.NONUSER 65 | extra = "User is not active." 66 | else: 67 | perms = Permission.USER 68 | 69 | if perms < min_level: 70 | title = "Insufficient permissions to access this resource" 71 | raise JsonApiException( 72 | title=title, 73 | detail=extra, 74 | status=403, 75 | ) 76 | 77 | kwargs["user"] = user 78 | kwargs["permission"] = perms 79 | return function(*args, **kwargs) 80 | 81 | return user_wrap_function 82 | 83 | return wrapper 84 | -------------------------------------------------------------------------------- /megaqc/rest_api/webarg_parser.py: -------------------------------------------------------------------------------- 1 | from querystring_parser import parser as qsp 2 | from webargs import core 3 | from webargs.flaskparser import FlaskParser 4 | 5 | 6 | class NestedQueryFlaskParser(FlaskParser): 7 | def parse_querystring(self, req, name, field): 8 | return core.get_value(qsp.parse(req.query_string), name, field) 9 | # return qsp.parse(req.query_string)[name] 10 | 11 | 12 | parser = NestedQueryFlaskParser() 13 | use_args = parser.use_args 14 | use_kwargs = parser.use_kwargs 15 | error_handler = parser.error_handler 16 | -------------------------------------------------------------------------------- /megaqc/scheduler.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import datetime 5 | import gzip 6 | import io 7 | import json 8 | import os 9 | import traceback 10 | from builtins import str 11 | 12 | from flask import current_app 13 | from flask_apscheduler import APScheduler 14 | 15 | from megaqc.api.utils import handle_report_data 16 | from megaqc.extensions import db 17 | from megaqc.model.models import Upload 18 | from megaqc.user.models import User 19 | 20 | scheduler = APScheduler() 21 | 22 | 23 | def init_scheduler(app): 24 | if not scheduler.running: 25 | scheduler.init_app(app) 26 | scheduler.start() 27 | 28 | 29 | def upload_reports_job(): 30 | with scheduler.app.app_context(): 31 | queued_uploads = ( 32 | db.session.query(Upload).filter(Upload.status == "NOT TREATED").all() 33 | ) 34 | for row in queued_uploads: 35 | user = db.session.query(User).filter(User.user_id == row.user_id).one() 36 | current_app.logger.info( 37 | "Beginning process of upload #{} from {}".format( 38 | row.upload_id, user.email 39 | ) 40 | ) 41 | row.status = "IN TREATMENT" 42 | db.session.add(row) 43 | db.session.commit() 44 | # Check if we have a gzipped file 45 | gzipped = False 46 | with open(row.path, "rb") as fh: 47 | # Check if we have a gzipped file 48 | file_start = fh.read(3) 49 | if file_start == b"\x1f\x8b\x08": 50 | gzipped = True 51 | try: 52 | if gzipped: 53 | with io.BufferedReader(gzip.open(row.path, "rb")) as fh: 54 | raw_data = fh.read().decode("utf-8") 55 | else: 56 | with io.open(row.path, "rb") as fh: 57 | raw_data = fh.read().decode("utf-8") 58 | data = json.loads(raw_data) 59 | # Now save the parsed JSON data to the database 60 | ret = handle_report_data(user, data) 61 | except Exception: 62 | ret = ( 63 | False, 64 | "
{}
".format(traceback.format_exc()), 65 | ) 66 | current_app.logger.error( 67 | "Error processing upload {}: {}".format( 68 | row.upload_id, traceback.format_exc() 69 | ) 70 | ) 71 | if ret[0]: 72 | row.status = "TREATED" 73 | row.message = "The document has been uploaded successfully" 74 | os.remove(row.path) 75 | else: 76 | if ret[1] == "Report already processed": 77 | current_app.logger.info( 78 | "Upload {} already being processed by another worker, skipping".format( 79 | row.upload_id 80 | ) 81 | ) 82 | continue 83 | row.status = "FAILED" 84 | row.message = "The document has not been uploaded : {0}".format(ret[1]) 85 | row.modified_at = datetime.datetime.utcnow() 86 | current_app.logger.info( 87 | "Finished processing upload #{} to state {}".format( 88 | row.upload_id, row.status 89 | ) 90 | ) 91 | db.session.add(row) 92 | db.session.commit() 93 | -------------------------------------------------------------------------------- /megaqc/static/css/tether.min.css: -------------------------------------------------------------------------------- 1 | .tether-element,.tether-element *,.tether-element :after,.tether-element :before,.tether-element:after,.tether-element:before{box-sizing:border-box}.tether-element{position:absolute;display:none}.tether-element.tether-open{display:block} -------------------------------------------------------------------------------- /megaqc/static/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /megaqc/static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /megaqc/static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /megaqc/static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /megaqc/static/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /megaqc/static/fonts/roboto/Roboto-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/fonts/roboto/Roboto-Black.ttf -------------------------------------------------------------------------------- /megaqc/static/fonts/roboto/Roboto-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/fonts/roboto/Roboto-BlackItalic.ttf -------------------------------------------------------------------------------- /megaqc/static/fonts/roboto/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/fonts/roboto/Roboto-Bold.ttf -------------------------------------------------------------------------------- /megaqc/static/fonts/roboto/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/fonts/roboto/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /megaqc/static/fonts/roboto/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/fonts/roboto/Roboto-Italic.ttf -------------------------------------------------------------------------------- /megaqc/static/fonts/roboto/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/fonts/roboto/Roboto-Light.ttf -------------------------------------------------------------------------------- /megaqc/static/fonts/roboto/Roboto-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/fonts/roboto/Roboto-LightItalic.ttf -------------------------------------------------------------------------------- /megaqc/static/fonts/roboto/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/fonts/roboto/Roboto-Medium.ttf -------------------------------------------------------------------------------- /megaqc/static/fonts/roboto/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/fonts/roboto/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /megaqc/static/fonts/roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/fonts/roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /megaqc/static/fonts/roboto/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/fonts/roboto/Roboto-Thin.ttf -------------------------------------------------------------------------------- /megaqc/static/fonts/roboto/Roboto-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/fonts/roboto/Roboto-ThinItalic.ttf -------------------------------------------------------------------------------- /megaqc/static/img/BarRandom-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/img/BarRandom-256.png -------------------------------------------------------------------------------- /megaqc/static/img/MegaQC_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/img/MegaQC_logo.png -------------------------------------------------------------------------------- /megaqc/static/img/SciLifeLab_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/img/SciLifeLab_logo.png -------------------------------------------------------------------------------- /megaqc/static/img/comparisons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/img/comparisons.png -------------------------------------------------------------------------------- /megaqc/static/img/distributions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/img/distributions.png -------------------------------------------------------------------------------- /megaqc/static/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/img/favicon.png -------------------------------------------------------------------------------- /megaqc/static/img/favicon.svg: -------------------------------------------------------------------------------- 1 | MultiQC_logo_circle -------------------------------------------------------------------------------- /megaqc/static/img/trends.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/megaqc/static/img/trends.png -------------------------------------------------------------------------------- /megaqc/static/js/libs/gridstack.jQueryUI.min.js: -------------------------------------------------------------------------------- 1 | !function(e){if("function"==typeof define&&define.amd)define(["jquery","lodash","gridstack","jquery-ui/data","jquery-ui/disable-selection","jquery-ui/focusable","jquery-ui/form","jquery-ui/ie","jquery-ui/keycode","jquery-ui/labels","jquery-ui/jquery-1-7","jquery-ui/plugin","jquery-ui/safe-active-element","jquery-ui/safe-blur","jquery-ui/scroll-parent","jquery-ui/tabbable","jquery-ui/unique-id","jquery-ui/version","jquery-ui/widget","jquery-ui/widgets/mouse","jquery-ui/widgets/draggable","jquery-ui/widgets/droppable","jquery-ui/widgets/resizable"],e);else if("undefined"!=typeof exports){try{jQuery=require("jquery")}catch(e){}try{_=require("lodash")}catch(e){}try{GridStackUI=require("gridstack")}catch(e){}e(jQuery,_,GridStackUI)}else e(jQuery,_,GridStackUI)}(function(a,n,r){window;function e(e){r.GridStackDragDropPlugin.call(this,e)}return r.GridStackDragDropPlugin.registerPlugin(e),((e.prototype=Object.create(r.GridStackDragDropPlugin.prototype)).constructor=e).prototype.resizable=function(e,r){if(e=a(e),"disable"===r||"enable"===r)e.resizable(r);else if("option"===r){var i=arguments[2],t=arguments[3];e.resizable(r,i,t)}else{var u=e.data("gs-resize-handles")?e.data("gs-resize-handles"):this.grid.opts.resizable.handles;e.resizable(n.extend({},this.grid.opts.resizable,{handles:u},{start:r.start||function(){},stop:r.stop||function(){},resize:r.resize||function(){}}))}return this},e.prototype.draggable=function(e,r){return e=a(e),"disable"===r||"enable"===r?e.draggable(r):e.draggable(n.extend({},this.grid.opts.draggable,{containment:this.grid.opts.isNested?this.grid.container.parent():null,start:r.start||function(){},stop:r.stop||function(){},drag:r.drag||function(){}})),this},e.prototype.droppable=function(e,r){return(e=a(e)).droppable(r),this},e.prototype.isDroppable=function(e,r){return e=a(e),Boolean(e.data("droppable"))},e.prototype.on=function(e,r,i){return a(e).on(r,i),this},e}); 2 | //# sourceMappingURL=gridstack.min.map -------------------------------------------------------------------------------- /megaqc/static/js/megaqc.js: -------------------------------------------------------------------------------- 1 | $(function(){ 2 | // Turn on the zero clipboard copy buttons 3 | new Clipboard('.btn-copy'); 4 | 5 | // Turn on the bootstrap tooltips 6 | $('[data-toggle="tooltip"]').tooltip(); 7 | 8 | // Focus the save-plot-modal title field when shown 9 | $('#save_plot_favourite_modal').on('shown.bs.modal', function (e) { 10 | $('#plot_favourite_title').focus(); 11 | }); 12 | }); 13 | 14 | 15 | // Save a favourite plot - triggered when submitting 16 | // the "Save Favourite" modal form 17 | function save_plot_favourite(plot_type, request_data){ 18 | 19 | // Check that we have a title 20 | if(!$('#plot_favourite_title').val().trim()){ 21 | $('#plot_favourite_title_feedback').show(); 22 | $('#plot_favourite_title').addClass('is-invalid'); 23 | return; 24 | } 25 | 26 | // Send the plot details to save 27 | window.ajax_update = $.ajax({ 28 | url: '/api/save_plot_favourite', 29 | type: 'post', 30 | data: JSON.stringify({ 31 | 'type': plot_type, 32 | 'request_data': request_data, 33 | 'title': $('#plot_favourite_title').val(), 34 | 'description': $('#plot_favourite_description').val() 35 | }), 36 | headers : { access_token:window.token }, 37 | dataType: 'json', 38 | contentType: 'application/json; charset=UTF-8', 39 | success: function(data){ 40 | if (data['success']){ 41 | $('#save_plot_favourite_modal').modal('hide'); 42 | $('#plot_favourite_title').val(''); 43 | $('#plot_favourite_description').val(''); 44 | toastr.success( 45 | 'Plot favourite saved!
Click to view all plot favourites.', 46 | null, 47 | { onclick: function() { window.location.href = "/plot_favourites/"; } } 48 | ); 49 | } 50 | // AJAX data['success'] was false 51 | else { 52 | console.log(data); 53 | $('#save_plot_favourite_modal').modal('hide'); 54 | toastr.error('There was an error whilst saving this plot.'); 55 | } 56 | }, 57 | error: function(data){ 58 | console.log(data); 59 | $('#save_plot_favourite_modal').modal('hide'); 60 | toastr.error('There was an error saving this plot.'); 61 | } 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /megaqc/static/js/members.js: -------------------------------------------------------------------------------- 1 | 2 | $("#password_submit").click(function(e){ 3 | passwords = []; 4 | e.preventDefault(); 5 | $('.pw_input').each(function(idx, el){ 6 | passwords.push(el.value); 7 | }); 8 | if (passwords[0] != passwords[1]){ 9 | alert("Passwords do not match"); 10 | } else { 11 | var data={"password":passwords[0]}; 12 | $.ajax({ 13 | url:"/api/set_password", 14 | type: 'post', 15 | data:JSON.stringify(data), 16 | headers : {access_token:window.token}, 17 | dataType: 'json', 18 | contentType:"application/json; charset=UTF-8", 19 | success: function(data){ 20 | $('
New password set
').appendTo('form').hide().slideDown(); 21 | } 22 | }); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /megaqc/static/js/plot_choice.js: -------------------------------------------------------------------------------- 1 | 2 | $(function(){ 3 | init_btns(); 4 | init_report_checkboxes(); 5 | init_datatable(); 6 | }); 7 | function init_datatable(){ 8 | 9 | $('#report_table').DataTable(); 10 | $('div.dataTables_filter input').addClass('form-control search search-query'); 11 | $('#project_table_filter').addClass('form-inline pull-right'); 12 | $("#project_table_filter").appendTo("h1"); 13 | 14 | } 15 | function init_report_checkboxes(){ 16 | $(".checkbox_report").click(function(e){ 17 | report_id = $(this).data('id'); 18 | if( $(this).is(':checked') ){ 19 | $.ajax({ 20 | url:"/api/get_samples_per_report", 21 | type: 'post', 22 | data:JSON.stringify({"report_id":report_id}), 23 | headers : {access_token:window.token}, 24 | dataType: 'json', 25 | contentType:"application/json; charset=UTF-8", 26 | success: function(data){ 27 | for (key in data){ 28 | $("#sample_table tbody").append(""+key+""+data[key]+""); 29 | } 30 | } 31 | }); 32 | }else{ 33 | $(".sample_row_"+report_id).remove(); 34 | 35 | } 36 | 37 | }); 38 | 39 | 40 | } 41 | function init_btns(){ 42 | $("#make_plot_btn").click(function(e){ 43 | selected_samples=[]; 44 | $(".sample_chbx:checked").each(function(idx){ 45 | selected_samples.push($(this).data('name')); 46 | } 47 | ); 48 | $.ajax({ 49 | url:"/api/get_report_plot", 50 | type: 'post', 51 | data:JSON.stringify({"samples":selected_samples, "plot_type":window.graph_type}), 52 | headers : {access_token:window.token}, 53 | dataType: 'json', 54 | contentType:"application/json; charset=UTF-8", 55 | success: function(json){ 56 | console.log(json); 57 | html=json['plot']; 58 | //$("#plot_modal_body").html(html); 59 | //$("#result_plot").modal(); 60 | $('#plot_location').html(html); 61 | } 62 | }); 63 | 64 | }); 65 | $(".plot_type_choice").each(function(idx){ 66 | $(this).click(function(e){ 67 | e.preventDefault(); 68 | $("#plot_type_btn").html($(this).data('type')+""); 69 | window.graph_type=$(this).data('type'); 70 | }); 71 | }); 72 | } 73 | -------------------------------------------------------------------------------- /megaqc/templates/401.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "layout.html" %} 3 | 4 | {% block page_title %}Unauthorized{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

401: Unauthorized

10 |

You are not authorized to see this page. Please log in or 11 | create a new account. 12 |

13 |
14 |
15 | {% endblock %} 16 | 17 | -------------------------------------------------------------------------------- /megaqc/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "layout.html" %} 3 | 4 | {% block page_title %}Page Not Found{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

404: Page Not Found

10 |

Sorry, that page doesn't exist.

11 |

Want to go home instead?

12 |
13 |
14 | {% endblock %} 15 | 16 | -------------------------------------------------------------------------------- /megaqc/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "layout.html" %} 3 | 4 | {% block page_title %}Server error{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

500: Internal Server Error

10 |

Sorry, something went wrong on our system.

11 |

If this keeps happening, please let the system administrator know.
12 | If you are the system administrator, check the server logs to see what's going wrong...

13 |
14 |
15 | {% endblock %} 16 | 17 | -------------------------------------------------------------------------------- /megaqc/templates/error.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "layout.html" %} 3 | 4 | {% block page_title %}Page Not Found{% endblock %} 5 | 6 | {% block content %} 7 |
8 |
9 |

{{ error.code }}

10 |

{{ error.description }}

11 |

Want to go home instead?

12 |
13 |
14 | {% endblock %} 15 | 16 | -------------------------------------------------------------------------------- /megaqc/templates/footer.html: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /megaqc/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block page_title %}MegaQC{% endblock %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% block css %}{% endblock %} 20 | 21 | 22 | 23 | 24 |
25 | {% block body %} 26 | 27 | {% include "nav.html" %} 28 | 29 | {% if self.header() %}
{% block header %}{% endblock %}
{% endif %} 30 |
31 | 32 | {% with messages = get_flashed_messages(with_categories=true) %} 33 | {% if messages %} 34 | {% for category, message in messages %} 35 | 41 | {% endfor %} 42 | {% endif %} 43 | {% endwith %} 44 | 45 | {% block content %}{% endblock %} 46 | 47 |
48 | 49 | 50 | {% include "footer.html" %} 51 | 52 | {% endblock %} 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 69 | {% block js %}{% endblock %} 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /megaqc/templates/layout_blank.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block page_title %}MegaQC{% endblock %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% block css %}{% endblock %} 20 | 21 | 22 | 23 | 24 | {% block body %} 25 | {% block content %}{% endblock %} 26 | {% endblock %} 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 42 | {% block js %}{% endblock %} 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /megaqc/templates/public/about.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |

About

5 |

MegaQC was written by Phil Ewels and Denis Moreno.

6 |

The website code is written using Flask, initially based upon a template created by 7 | Steven Loria for use with the 8 | cookiecutter 9 | package by Audrey Roy. 10 |

11 | {% endblock %} 12 | 13 | -------------------------------------------------------------------------------- /megaqc/templates/public/dashboard.html: -------------------------------------------------------------------------------- 1 | {% if raw %} 2 | {% extends 'layout_blank.html' %} 3 | {% else %} 4 | {% extends 'layout.html' %} 5 | {% endif %} 6 | 7 | {% block content %} 8 | 9 | {% if not raw %} 10 | 11 |

{{ dashboard.title }}

12 | 13 |

View the dashboard in an empty page without MegaQC navigation and footer here: 14 | {{ request.base_url }}/raw

15 | 16 |
17 |
18 | 19 | {% endif %} 20 | 21 |
22 | {% for d in dashboard.data %} 23 |
24 | {% if d['type'] == 'plot' and 'plot_id' in d %} 25 |
26 |
27 | 28 | Loading... 29 |
30 |
31 | {% elif 'html_contents' in d %} 32 | {{ d['html_contents'] | safe }} 33 | {% else %} 34 | {{ d }} 35 | {% endif %} 36 |
37 | {% endfor %} 38 |
39 | 40 | {% if not raw %} 41 | 42 |
43 |
44 | {% endif %} 45 | 46 | {% endblock %} 47 | 48 | 49 | {% block js %} 50 | 80 | {% endblock %} 81 | -------------------------------------------------------------------------------- /megaqc/templates/public/filter_samples_selectbox.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | Filter Samples 4 | 5 | {{ num_samples }} samples 6 | 7 | 11 |

12 |
13 |
14 |
15 | 20 |
21 |
22 |
23 |
24 | {% for sfg in sample_filters.keys() | sort %} 25 | {% set sfg_loop = loop %} 26 |
27 |
28 | {% for sf in sample_filters[sfg] %} 29 | 30 | {% endfor %} 31 |
32 |
33 | {% endfor %} 34 |
35 |
36 |
37 |
38 |
39 | -------------------------------------------------------------------------------- /megaqc/templates/public/home.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "layout.html" %} 3 | {% block content %} 4 | 5 |
6 |
7 |
8 |

9 |
10 |
11 |

MegaQC is a web application that collects results from multiple runs of MultiQC and allows bulk visualisation.

12 |

Please refer to the MegaQC website for further information: https://megaqc.info

13 |

If you are unfamiliar with MultiQC, please have a look at: http://multiqc.info

14 |
15 |
16 | Data available from
17 | {{ num_samples }} samples, {{ num_reports }} reports
18 | {% if num_uploads_processing %} 19 | 20 | {{ num_uploads_processing }} upload{% if num_uploads_processing > 1 %}s{% endif %} processing 21 | 22 | {% endif %} 23 |
24 |
25 |
26 |

MegaQC is a web application that collects results from multiple runs of MultiQC and allows bulk visualisation.

27 |

See the MegaQC GitHub repository for installation instructions and documentation: https://github.com/MultiQC/MegaQC/

28 |
29 |
30 | 31 |

Choose a plot type

32 | 33 | 79 | 80 | {% endblock %} 81 | -------------------------------------------------------------------------------- /megaqc/templates/public/login.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block content %} 3 | 4 |

Log In

5 |
6 | 7 |
8 | {{ form.username.label(class_="col-sm-2 control-label") }} 9 |
10 | {{ form.username(placeholder="Username", class_="form-control mr-sm-2") }} 11 |
12 |
13 |
14 | {{ form.password.label(class_="col-sm-2 control-label") }} 15 |
16 | {{ form.password(placeholder="Password", class_="form-control mr-sm-2") }} 17 |
18 |
19 |
20 |
21 | 22 |
23 |
24 |
25 |
Not registered? Click here to create an account.
26 | 27 | {% endblock %} -------------------------------------------------------------------------------- /megaqc/templates/public/plot_choice.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "layout.html" %} 3 | {% block css %} 4 | 5 | {% endblock %} 6 | {% block content %} 7 |

Generate new Plot

8 |
9 |

Plot type

10 |

11 |

20 |

21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% for report in reports %} 31 | 32 | 33 | 34 | 35 | 36 | 37 | {% endfor %} 38 | 39 |
Report IdReport DateUserSelect
{{report.hash}}{{report.created_at}}{{db.session.query(User).filter(User.user_id == report.user_id).first().username}}
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |
Sample NameReport NameSelect
49 |
50 |
51 | 52 |
53 |
54 |
55 | {% endblock %} 56 | {% block js %} 57 | 58 | 59 | 60 | {% endblock %} 61 | -------------------------------------------------------------------------------- /megaqc/templates/public/plot_type.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "layout.html" %} 3 | {% block content %} 4 | 5 |

Choose a plot type

6 | 7 | 53 | 54 | 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /megaqc/templates/public/react.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "layout.html" %} 3 | {% block content %} 4 |
5 | {% endblock %} 6 | {% block js %} 7 | 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /megaqc/templates/public/register.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "layout.html" %} 3 | {% block content %} 4 |

Create an account

5 |
6 | 7 |
8 | {{ form.username.label(class_="col-sm-2 control-label") }} 9 |
10 | {{ form.username(placeholder="Username", class_="form-control") }} 11 |
12 |
13 |
14 | {{ form.first_name.label(class_="col-sm-2 control-label") }} 15 |
16 | {{ form.first_name(placeholder="First Name", class_="form-control") }} 17 |
18 |
19 |
20 | {{ form.last_name.label(class_="col-sm-2 control-label") }} 21 |
22 | {{ form.last_name(placeholder="Last Name", class_="form-control") }} 23 |
24 |
25 |
26 | {{ form.email.label(class_="col-sm-2 control-label") }} 27 |
28 | {{ form.email(placeholder="Email", class_="form-control") }} 29 |
30 |
31 |
32 | {{ form.password.label(class_="col-sm-2 control-label") }} 33 |
34 | {{ form.password(placeholder="Password", class_="form-control") }} 35 |
36 |
37 |
38 | {{ form.confirm.label(class_="col-sm-2 control-label") }} 39 |
40 | {{ form.confirm(placeholder="Password (again)", class_="form-control") }} 41 |
42 |
43 |
44 |
45 | 46 |
47 |
48 |
49 |
Already registered? Click here to login.
50 | {% endblock %} 51 | 52 | -------------------------------------------------------------------------------- /megaqc/templates/public/save_plot_favourite_modal.html: -------------------------------------------------------------------------------- 1 | 31 | -------------------------------------------------------------------------------- /megaqc/templates/users/change_password.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "layout.html" %} 3 | {% block content %} 4 |

Change Your Password

5 |
6 |
7 | {{ form.password.label(class_="col-sm-2 col-form-label") }} 8 |
9 | {{ form.password(placeholder="New Password", class_="form-control pw_input") }} 10 |
11 |
12 |
13 | {{ form.confirm.label(class_="col-sm-2 col-form-label") }} 14 |
15 | {{form.confirm(placeholder="Re-type Password", class_="form-control pw_input")}} 16 |
17 |
18 |
19 |
20 | 21 |
22 |
23 |
24 | {% endblock %} 25 | 26 | {% block js %} 27 | 28 | 29 | {% endblock %} 30 | 31 | -------------------------------------------------------------------------------- /megaqc/templates/users/dashboards.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | {% block content %} 3 |

My Dashboards

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% if dashboards|length == 0 %} 16 | 17 | 20 | 21 | {% endif %} 22 | {% for dash in dashboards %} 23 | 24 | 25 | 26 | 27 | 32 | 33 | {% endfor %} 34 | 35 |
TitlePublicCreatedActions
18 | No dashboards found.. 19 |
{{ dash['title'] }}{{ 'Yes' if dash['is_public'] else 'No' }}{{ dash['created_at'].strftime('%Y-%m-%d %H:%M') }} 28 | View 29 | Edit 30 | Delete 31 |
36 | 37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /megaqc/templates/users/manage_users.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "layout.html" %} 3 | {% block content %} 4 |

Manage Users

5 | 6 |
7 | 8 |

Add new user

9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
UsernameEmailFirst NameLast NameActiveAdminAdd
29 | 30 | 31 | 32 | 33 |

Registered users

34 | {{ form.csrf_token}} 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | {% for user in users_data %} 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 60 | 61 | {% endfor %} 62 |
UsernameEmailFirst NameLast NameAPI TokenActiveAdminActions
56 | 57 | 58 | 59 |
63 | 64 |
65 | 66 | {% endblock %} 67 | 68 | {% block js %} 69 | 70 | 71 | {% endblock %} 72 | -------------------------------------------------------------------------------- /megaqc/templates/users/multiqc_config.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "layout.html" %} 3 | {% block content %} 4 |

MultiQC Configuration

5 | 6 |

Your identification token

7 |

To push data to MegaQC from MultiQC, you need an API authentication token. This needs to be added 8 | to the MultiQC configuration. To do this, copy the following to your user MultiQC configuration 9 | file in ~/.multiqc_config.yaml:

10 |
11 |
12 | 13 | ~/.multiqc_config.yaml 14 |
15 |
16 |
megaqc_url: {{url_for('api.queue_multiqc_data', _external=True)}}
megaqc_access_token: {{current_user.api_token}}
17 |
18 |
19 |

Note that this identification key is specific to you and should not be shared with any other users.

20 | 21 |

Troubleshooting

22 |

If you're having difficulties getting MultiQC to send data, you can test the connection using this command:

23 |
24 |
25 | 26 | Test connection command 27 |
28 |
29 |
curl -H "access_token: {{current_user.api_token}}" -H "Content-Type: application/json" {{url_for('api.test', _external=True)}}
30 |
31 |
32 |

You should get a response that looks like this:

33 |
34 |
35 |
{
"name": "{{current_user.username}}"
}
36 |
37 |
38 |

If the command times out or returns an error, this may indicate a problem with your firewall or connection.

39 | {% endblock %} 40 | -------------------------------------------------------------------------------- /megaqc/templates/users/organize_filters.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "layout.html" %} 3 | {% block content %} 4 |

Edit Sample Filters

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% for sfgroup in sample_filters.keys() | sort %} 15 | {% for sf in sample_filters[sfgroup] %} 16 | {% set ns = sample_filter_counts.get(sf['id'], '?') %} 17 | 18 | 19 | 20 | 25 | 29 | 30 | {% endfor %} 31 | {% endfor %} 32 |
Filter GroupFilter NameMatching SamplesActions
{{ sfgroup }}{{ sf['name'] }} 21 | 22 | {{ ns }} samples 23 | 24 | 26 | 27 | 28 |
33 | 34 | {% include "public/filter_samples_modal.html" %} 35 | 36 | {% endblock %} 37 | {% block js %} 38 | 41 | {% endblock %} 42 | -------------------------------------------------------------------------------- /megaqc/templates/users/plot_favourite.html: -------------------------------------------------------------------------------- 1 | {% if raw %} 2 | {% extends 'layout_blank.html' %} 3 | {% else %} 4 | {% extends 'layout.html' %} 5 | {% endif %} 6 | 7 | {% block content %} 8 | 9 | {% if raw %} 10 | 11 | {{ plot_data['plot_html']|safe }} 12 | 13 | {% else %} 14 |

{{ plot_data['title'] }}

15 |

{{ plot_data['description'] }}

16 | 17 |
18 |

19 | {{ plot_data['title'] }} 20 | 21 | Raw plot: 22 | {{ request.base_url }}/raw 23 | 24 |

25 |
26 |
27 | {{ plot_data['plot_html']|safe }} 28 |
29 |
30 |
31 | 32 | {% endif %} 33 | 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /megaqc/templates/users/profile.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "layout.html" %} 3 | {% block content %} 4 |

Profile Page

5 |

Welcome to your profile page! Here you can find information about how many MultiQC reports 6 | you've submitted, how to configure MultiQC, you can change your password and all kinds of other stuff!

7 |

Your Account

8 |
9 |
Your name:
{{current_user.first_name}} {{current_user.last_name}}
10 |
Your username:
{{current_user.username}}
11 |
Your e-mail address:
{{current_user.email}}
12 |
Your password:
Click here to change your password
13 |
MultiQC API Key:
Click here for instructions on how to configure MultiQC
14 |
15 | 16 |

MegaQC Statistics

17 |
18 |
Number of reports submitted:
?
19 |
Number of samples submitted:
?
20 |
21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /megaqc/templates/users/queued_uploads.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends "layout.html" %} 3 | {% block content %} 4 |

Pending Uploads

5 |

This page shows the MultiQC reports that have uploaded JSON but have not yet been successfully 6 | imported into the MegaQC database. To see reports that have been successfully imported, 7 | see the Edit Reports page. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% for upload in uploads %} 20 | 21 | 22 | 23 | 24 | 25 | 26 | {% endfor %} 27 | 28 |
IDStatusMessageUpload Date
{{ upload.upload_id }}{{ upload.status }}{{ upload.message | safe }}{{ upload.upload_date }}
29 | 30 | {% endblock %} 31 | -------------------------------------------------------------------------------- /megaqc/user/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | The user module. 4 | """ 5 | from . import views # noqa 6 | -------------------------------------------------------------------------------- /megaqc/user/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | User views. 4 | """ 5 | from flask import Blueprint, abort, render_template 6 | from flask_login import current_user, login_required 7 | 8 | from megaqc.extensions import db 9 | from megaqc.user.forms import AdminForm, PasswordChangeForm 10 | from megaqc.user.models import User 11 | 12 | blueprint = Blueprint("user", __name__, url_prefix="/users", static_folder="../static") 13 | 14 | 15 | @blueprint.route("/") 16 | @login_required 17 | def profile(): 18 | """ 19 | Show user profile. 20 | """ 21 | return render_template("users/profile.html") 22 | 23 | 24 | @blueprint.route("/multiqc_config") 25 | @login_required 26 | def multiqc_config(): 27 | """ 28 | Instructions for MultiQC configuration. 29 | """ 30 | return render_template("users/multiqc_config.html") 31 | 32 | 33 | @blueprint.route("/password") 34 | @login_required 35 | def change_password(): 36 | """ 37 | Change user password. 38 | """ 39 | form = PasswordChangeForm() 40 | return render_template("users/change_password.html", form=form) 41 | 42 | 43 | @blueprint.route("/admin/users") 44 | @login_required 45 | def manage_users(): 46 | if not current_user.is_admin: 47 | abort(403) 48 | else: 49 | users_data = db.session.query(User).all() 50 | form = AdminForm() 51 | return render_template( 52 | "users/manage_users.html", users_data=users_data, form=form 53 | ) 54 | -------------------------------------------------------------------------------- /megaqc/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Helper utilities and decorators. 4 | """ 5 | from flask import flash 6 | 7 | 8 | def flash_errors(form, category="warning"): 9 | """ 10 | Flash all errors for a form. 11 | """ 12 | for field, errors in list(form.errors.items()): 13 | for error in errors: 14 | flash("{0} - {1}".format(getattr(form, field).label.text, error), category) 15 | -------------------------------------------------------------------------------- /megaqc/utils/config_defaults.yaml: -------------------------------------------------------------------------------- 1 | ################################################################# 2 | # MegaQC Defaults 3 | ################################################################# 4 | # This file contains the default configuration options 5 | # for MegaQC. IT SHOULD NOT BE EDITED. If you want to 6 | # change any of these config options, create a new file 7 | # in any of the following locations: 8 | # 1. /megaqc_config.yaml (not pip or conda) 9 | # 2. ~/.megaqc_config.yaml 10 | # 3. /megaqc_config.yaml 11 | ################################################################# 12 | 13 | # Customise the report metadata fields 14 | # nicename: Specify user-friendly name for field 15 | # priority: customise position in list. Default priority: 0 16 | # hidden: True to remove from the filter-samples UI 17 | report_metadata_fields: 18 | config_title: 19 | priority: 5 20 | nicename: Report title 21 | config_subtitle: 22 | priority: 4 23 | nicename: Report subtitle 24 | config_report_comment: 25 | priority: 3 26 | nicename: Report comment 27 | config_intro_text: 28 | priority: 2 29 | nicename: Report intro text 30 | config_creation_date: 31 | priority: 1 32 | nicename: Report creation date & time 33 | config_short_version: 34 | priority: -1 35 | nicename: MultiQC version 36 | config_version: 37 | hidden: True 38 | config_script_path: 39 | hidden: True 40 | config_git_hash: 41 | hidden: True 42 | 43 | sample_metadata_fields: {} 44 | 45 | # Same defaults as HighCharts for consistency 46 | default_plot_colors: 47 | - "#7cb5ec" 48 | - "#434348" 49 | - "#90ed7d" 50 | - "#f7a35c" 51 | - "#8085e9" 52 | - "#f15c80" 53 | - "#e4d354" 54 | - "#2b908f" 55 | - "#f45b5b" 56 | - "#91e8e1" 57 | -------------------------------------------------------------------------------- /megaqc/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from environs import Env 4 | 5 | from megaqc.app import create_app 6 | from megaqc.settings import DevConfig, ProdConfig, TestConfig 7 | 8 | env = Env() 9 | 10 | if env.bool("FLASK_DEBUG", False): 11 | CONFIG = DevConfig() 12 | elif env.bool("MEGAQC_PRODUCTION", False): 13 | CONFIG = ProdConfig() 14 | else: 15 | CONFIG = TestConfig() 16 | 17 | app = create_app(CONFIG) 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "megaqc", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "watch": "webpack --mode development --watch", 8 | "build": "webpack --mode production" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "devDependencies": { 14 | "@babel/core": "^7.12.9", 15 | "@babel/plugin-transform-runtime": "^7.12.1", 16 | "@babel/preset-env": "^7.12.7", 17 | "@babel/preset-react": "^7.12.7", 18 | "@date-io/moment": "^2.10.6", 19 | "@holidayextras/jsonapi-client": "^1.0.0", 20 | "@material-ui/core": "^4.11.2", 21 | "@material-ui/pickers": "^3.2.10", 22 | "axios": "^0.21.0", 23 | "babel-loader": "^8.2.2", 24 | "classnames": "^2.2.6", 25 | "css-loader": "^5.0.1", 26 | "date-fns": "^2.16.1", 27 | "formik": "https://pkg.csb.dev/jaredpalmer/formik/commit/157603ab/formik", 28 | "formik-effect": "^1.2.0", 29 | "jsoneditor": "^9.1.4", 30 | "jsoneditor-react": "^3.1.0", 31 | "just-debounce-it": "^1.1.0", 32 | "lodash": "^4.17.20", 33 | "moment": "^2.29.1", 34 | "plotly.js": "^1.58.2", 35 | "prop-types": "^15.7.2", 36 | "ra-jsonapi-client": "github:tmiguelt/ra-jsonapi-client#github-publish", 37 | "react": "^16.14.0", 38 | "react-admin": "^3.10.2", 39 | "react-datepicker": "^3.3.0", 40 | "react-dom": "^16.14.0", 41 | "react-plotly.js": "^2.5.1", 42 | "reactstrap": "^8.7.1", 43 | "regenerator-runtime": "^0.13.7", 44 | "style-loader": "^1.3.0", 45 | "url-loader": "^4.1.1", 46 | "webpack": "^4.44.2", 47 | "webpack-cli": "^3.3.12", 48 | "webpack-dev-server": "^3.11.0", 49 | "yup": "^0.32.8" 50 | }, 51 | "dependencies": { 52 | "@rjsf/bootstrap-4": "^5.6.2", 53 | "@rjsf/core": "^5.6.2", 54 | "@rjsf/utils": "^5.6.2", 55 | "@rjsf/validator-ajv8": "^5.6.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /prestart.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | # This is used by the docker image. You can ignore this for regular use 3 | 4 | # Let the DB start 5 | sleep 10 6 | 7 | # Create the DB, ignoring errors, such as if the database already exists 8 | megaqc initdb || true 9 | 10 | # Run migrations 11 | cd megaqc 12 | export FLASK_APP=wsgi.py 13 | flask db upgrade 14 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "megaqc" 3 | version = "0.3.0" 4 | description = "Collect and visualise data across multiple MultiQC runs" 5 | authors = ["Phil Ewels"] 6 | license = "GPLv3" 7 | readme = "README.rst" 8 | keywords = [ 9 | "bioinformatics", 10 | "biology", 11 | "sequencing", 12 | "NGS", 13 | "next generation sequencing", 14 | "quality control", 15 | ] 16 | homepage = "https://megaqc.info/" 17 | repository = "https://github.com/MultiQC/MegaQC" 18 | documentation = "https://megaqc.info/docs/index.html" 19 | classifiers = [ 20 | "Development Status :: 4 - Beta", 21 | "Environment :: Console", 22 | "Environment :: Web Environment", 23 | "Intended Audience :: Science/Research", 24 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 25 | "Natural Language :: English", 26 | "Operating System :: MacOS :: MacOS X", 27 | "Operating System :: POSIX", 28 | "Operating System :: Unix", 29 | "Programming Language :: Python", 30 | "Programming Language :: JavaScript", 31 | "Topic :: Scientific/Engineering", 32 | "Topic :: Scientific/Engineering :: Bio-Informatics", 33 | "Topic :: Scientific/Engineering :: Visualization", 34 | ] 35 | packages = [{ include = "megaqc" }] 36 | 37 | [tool.poetry.dependencies] 38 | python = "^3.8" 39 | argon2-cffi = "20.1" 40 | click = "^7.0" 41 | Flask-APScheduler = "^1.11" 42 | Flask-Caching = "^1.9" 43 | Flask-DebugToolbar = "^0.11" 44 | Flask-Login = "^0.5" 45 | Flask-SQLAlchemy = "^2.4" 46 | Flask-WTF = "^0.14" 47 | Flask = "^1.1" 48 | future = "^0.18" 49 | itsdangerous = "^1.1" 50 | Jinja2 = "^2.11" 51 | markdown = "^3.3" 52 | multiqc = "^1.9" 53 | numpy = "^1.14" 54 | passlib = "^1.7" 55 | plotly = ">2,<5" 56 | pyyaml = "^5.1" 57 | SQLAlchemy = "^1.1" 58 | Werkzeug = ">0.14,<2.0" 59 | WTForms = { version = "^2.1", extras = ["email"] } 60 | flask_restful = "^0.3" 61 | flask-marshmallow = "^0.10" 62 | marshmallow = "^3.0" 63 | marshmallow-sqlalchemy = ">0.17,<0.26" 64 | flask-uploads = "^0.2" 65 | marshmallow-jsonapi = "^0.23.2" 66 | outlier-utils = "^0.0.3" 67 | webargs = "^5.5" # do not upgrade, since it breaks parsing 68 | querystring-parser = "^1.2" 69 | scipy = "^1.5" 70 | flatten_json = "^0.1" 71 | flapison = "^0.30" 72 | Flask-Migrate = "^2.5" 73 | environs = "^9.2.0" 74 | MarkupSafe = "<2.1.0" 75 | scikit-learn = "^1.2.0" 76 | marshmallow-jsonschema = "^0.13.0" 77 | marshmallow-polyfield = "^5.11" 78 | pytest = { version = "^7.2", optional = true } 79 | WebTest = { version = "^2.0", optional = true } 80 | factory-boy = { version = ">2.12,<4.0", optional = true } 81 | livereload = { version = "^2.5", optional = true } 82 | pre-commit = { version = "*", optional = true } 83 | wheel = { version = "^0.30", optional = true } 84 | psycopg2 = { version = "^2.6", optional = true } 85 | gunicorn = { version = ">19.7,<21.0", optional = true } 86 | 87 | [tool.poetry.extras] 88 | dev = [ 89 | "pytest", 90 | "WebTest", 91 | "factory-boy", 92 | "livereload", 93 | "pre-commit", 94 | ] 95 | deploy = ["wheel"] 96 | prod = ["psycopg2", "gunicorn"] 97 | 98 | [tool.poetry.scripts] 99 | megaqc = "megaqc.cli:main" 100 | 101 | [build-system] 102 | requires = ["poetry-core"] 103 | build-backend = "poetry.core.masonry.api" 104 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Included because many Paas's require a requirements.txt file in the project root 2 | . 3 | -------------------------------------------------------------------------------- /src/admin/components/defaultForm.js: -------------------------------------------------------------------------------- 1 | // A copy of SimpleForm, but loads default form values from the query string 2 | import React from "react"; 3 | import { SimpleForm } from "react-admin"; 4 | import { parse } from "query-string"; 5 | import set from "lodash/set"; 6 | 7 | export default function DefaultForm(props) { 8 | const defaults = {}; 9 | try { 10 | const search = parse(props.location.search); 11 | const parsed = JSON.parse(search.defaults); 12 | for (let key of Object.keys(parsed)) { 13 | set(defaults, key, parsed[key]); 14 | } 15 | } catch {} 16 | 17 | return ; 18 | } 19 | -------------------------------------------------------------------------------- /src/admin/components/jsonField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { JsonEditor as Editor } from "jsoneditor-react"; 3 | import "jsoneditor-react/es/editor.min.css"; 4 | import get from "lodash/get"; 5 | import { Labeled } from "react-admin"; 6 | import { useField } from "react-final-form"; 7 | 8 | export function JsonInput({ source, record, resource, basePath, editorProps }) { 9 | const { 10 | input: { onChange, value }, 11 | meta: { touched, error }, 12 | } = useField(source); 13 | const finalEditorProps = Object.assign( 14 | { 15 | search: false, 16 | mode: "code", 17 | statusBar: false, 18 | allowedModes: ["tree", "code"], 19 | }, 20 | editorProps 21 | ); 22 | return ( 23 | 31 | 32 | 33 | ); 34 | } 35 | 36 | export function JsonField({ 37 | source, 38 | record, 39 | resource, 40 | basePath, 41 | editorProps = {}, 42 | }) { 43 | const finalEditorProps = Object.assign( 44 | { 45 | search: false, 46 | statusBar: false, 47 | mode: "view", 48 | }, 49 | editorProps 50 | ); 51 | 52 | const value = get(record, source); 53 | return ( 54 | 62 | 63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/admin/components/layout.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { AppBar, Layout } from "react-admin"; 3 | import Typography from "@material-ui/core/Typography"; 4 | import { makeStyles } from "@material-ui/core/styles"; 5 | import Icon from "@material-ui/core/Icon"; 6 | import Link from "@material-ui/core/Link"; 7 | 8 | const useStyles = makeStyles({ 9 | title: { 10 | flex: 1, 11 | textOverflow: "ellipsis", 12 | whiteSpace: "nowrap", 13 | overflow: "hidden", 14 | }, 15 | spacer: { 16 | flex: 1, 17 | }, 18 | icon: { 19 | height: "1.2em", 20 | }, 21 | }); 22 | 23 | export function MegaQcAppBar(props) { 24 | const classes = useStyles(); 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 |   MegaQC - 34 | 35 | 36 | 37 | ); 38 | } 39 | 40 | export function MegaQcLayout(props) { 41 | return ; 42 | } 43 | -------------------------------------------------------------------------------- /src/admin/components/resourceLink.js: -------------------------------------------------------------------------------- 1 | import { Button, Link } from "react-admin"; 2 | import { useForm, useField } from "react-final-form"; 3 | import React from "react"; 4 | import { titleize } from "inflection"; 5 | 6 | /** 7 | * 8 | * @param reference The resource to link to 9 | * @param source The relationship key on the current resource which links to the 10 | * destination resource 11 | * @param dest The relationship key on the destination resource 12 | */ 13 | export default function ResourceLink({ reference, source, dest, children }) { 14 | // const form = useForm(); 15 | // const localValue = form.getFieldState(source); 16 | const localValue = useField(source); 17 | const query = encodeURIComponent( 18 | JSON.stringify({ 19 | [dest]: localValue.input.value, 20 | }) 21 | ); 22 | const title = titleize(reference); 23 | return ( 24 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/admin/dashboards.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | BooleanField, 4 | BooleanInput, 5 | Datagrid, 6 | DateField, 7 | DateInput, 8 | Edit, 9 | List, 10 | Show, 11 | SimpleForm, 12 | SimpleShowLayout, 13 | TextField, 14 | TextInput, 15 | } from "react-admin"; 16 | import { JsonInput, JsonField } from "./components/jsonField"; 17 | 18 | export const DashboardList = (props) => ( 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | 30 | export const DashboardShow = (props) => ( 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | ); 42 | 43 | export const DashboardEdit = (props) => ( 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | ); 55 | -------------------------------------------------------------------------------- /src/admin/dataType.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Create, 4 | Datagrid, 5 | Edit, 6 | List, 7 | Show, 8 | SimpleForm, 9 | SimpleShowLayout, 10 | TextField, 11 | TextInput, 12 | } from "react-admin"; 13 | 14 | export const DataTypeList = (props) => ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | 24 | export const DataTypeEdit = (props) => ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | 34 | export const DataTypeShow = (props) => ( 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | 44 | export const DataTypeCreate = (props) => ( 45 | 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | -------------------------------------------------------------------------------- /src/admin/favourite.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Datagrid, 4 | DateField, 5 | DateInput, 6 | Edit, 7 | List, 8 | Show, 9 | SimpleForm, 10 | SimpleShowLayout, 11 | TextField, 12 | TextInput, 13 | } from "react-admin"; 14 | import { JsonInput, JsonField } from "./components/jsonField"; 15 | 16 | export const FavouriteList = (props) => ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | 28 | export const FavouriteShow = (props) => ( 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | 41 | export const FavouriteEdit = (props) => ( 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ); 53 | -------------------------------------------------------------------------------- /src/admin/filterGroup.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Datagrid, List, TextField } from "react-admin"; 3 | 4 | export const FilterGroupList = (props) => ( 5 | 6 | 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /src/admin/meta.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | AutocompleteInput, 4 | Create, 5 | Datagrid, 6 | Edit, 7 | List, 8 | ReferenceField, 9 | ReferenceInput, 10 | Show, 11 | SimpleForm, 12 | SimpleShowLayout, 13 | TextField, 14 | TextInput, 15 | } from "react-admin"; 16 | 17 | import DefaultForm from "./components/defaultForm"; 18 | 19 | export const ReportMetaList = (props) => ( 20 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 35 | ); 36 | 37 | export const ReportMetaShow = (props) => ( 38 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | ); 54 | 55 | export const ReportMetaEdit = (props) => ( 56 | 57 | 58 | 59 | 60 | 61 | {}} 63 | label="Report" 64 | source="report.id" 65 | reference="reports" 66 | > 67 | 68 | 69 | 70 | 71 | ); 72 | 73 | export const ReportMetaCreate = (props) => ( 74 | 75 | 76 | 77 | 78 | {}} 80 | label="Report" 81 | source="report.id" 82 | reference="reports" 83 | > 84 | 85 | 86 | 87 | 88 | ); 89 | -------------------------------------------------------------------------------- /src/admin/report.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Datagrid, 4 | DateField, 5 | DateInput, 6 | Edit, 7 | EditButton, 8 | List, 9 | ReferenceField, 10 | ReferenceManyField, 11 | Show, 12 | ShowButton, 13 | SimpleForm, 14 | SimpleShowLayout, 15 | TextField, 16 | TextInput, 17 | } from "react-admin"; 18 | import ResourceLink from "./components/resourceLink"; 19 | 20 | export const ReportList = (props) => ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | 41 | export const ReportEdit = (props) => ( 42 | 43 | 44 | 45 | 46 | 47 | 48 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | ); 77 | 78 | export const ReportShow = (props) => ( 79 | 80 | 81 | 82 | 83 | 84 | 85 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | ); 113 | -------------------------------------------------------------------------------- /src/admin/sample.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | AutocompleteInput, 4 | Datagrid, 5 | Edit, 6 | EditButton, 7 | List, 8 | Pagination, 9 | ReferenceField, 10 | ReferenceInput, 11 | ReferenceManyField, 12 | Show, 13 | ShowButton, 14 | SimpleForm, 15 | SimpleShowLayout, 16 | TextField, 17 | TextInput, 18 | } from "react-admin"; 19 | 20 | import ResourceLink from "./components/resourceLink"; 21 | 22 | export const SampleList = (props) => ( 23 | 24 | 25 | 26 | 27 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | 41 | export const SampleShow = (props) => { 42 | return ( 43 | 44 | 45 | 46 | 47 | 53 | 54 | 55 | } 60 | > 61 | 62 | 63 | 64 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | ); 79 | }; 80 | 81 | export const SampleEdit = (props) => ( 82 | 83 | 84 | 85 | 86 | {}} 88 | label="Report" 89 | source="report.id" 90 | reference="reports" 91 | > 92 | 93 | 94 | 95 | 96 | 97 | 98 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | ); 114 | -------------------------------------------------------------------------------- /src/admin/sampleData.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Create, 4 | Datagrid, 5 | Edit, 6 | EditButton, 7 | List, 8 | ReferenceField, 9 | ReferenceInput, 10 | SelectInput, 11 | Show, 12 | ShowButton, 13 | SimpleForm, 14 | SimpleShowLayout, 15 | TextField, 16 | TextInput, 17 | } from "react-admin"; 18 | import DefaultForm from "./components/defaultForm"; 19 | 20 | export const DataList = (props) => ( 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | 47 | export const DataShow = (props) => ( 48 | 49 | 50 | 51 | 52 | 58 | 59 | 60 | 66 | 67 | 68 | 69 | 70 | ); 71 | 72 | export const DataEdit = (props) => ( 73 | 74 | 75 | 76 | 77 | {}} 79 | label="Type" 80 | source="data_type.id" 81 | reference="data_types" 82 | > 83 | 84 | 85 | {}} 87 | label="Sample" 88 | source="sample.id" 89 | reference="samples" 90 | > 91 | 92 | 93 | 94 | 95 | ); 96 | 97 | export const DataCreate = (props) => ( 98 | 99 | 100 | 101 | {}} 103 | label="Type" 104 | source="data_type.id" 105 | reference="data_types" 106 | > 107 | 108 | 109 | {}} 111 | label="Sample" 112 | source="sample.id" 113 | reference="samples" 114 | > 115 | 116 | 117 | 118 | 119 | ); 120 | -------------------------------------------------------------------------------- /src/admin/upload.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Datagrid, 4 | DateField, 5 | DateInput, 6 | Edit, 7 | List, 8 | Show, 9 | SimpleForm, 10 | SimpleShowLayout, 11 | TextField, 12 | TextInput, 13 | } from "react-admin"; 14 | 15 | export const UploadList = (props) => ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | 28 | export const UploadShow = (props) => ( 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | 41 | export const UploadEdit = (props) => ( 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ); 53 | -------------------------------------------------------------------------------- /src/trend/DateRangeField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Input, FormFeedback } from "reactstrap"; 3 | import { ErrorMessage, getIn } from "formik"; 4 | import { format, parse } from "date-fns"; 5 | import DatePicker from "react-datepicker"; 6 | import { DATE_FORMAT } from "../util/filter"; 7 | 8 | import "react-datepicker/dist/react-datepicker.css"; 9 | 10 | export default function DateRangeField(props) { 11 | const { field, form, type, outputFormat, ...rest } = props; 12 | const touched = getIn(form.touched, field.name); 13 | const errors = getIn(form.errors, field.name); 14 | const invalid = touched && errors; 15 | const valid = touched && !errors; 16 | 17 | // We're storing the date in the form in this format, so we have to parse it out first 18 | const [startDate, endDate] = field.value.map((d) => { 19 | const parsed = parse(d, DATE_FORMAT, new Date()); 20 | if (isNaN(parsed)) return new Date(); 21 | else return parsed; 22 | }); 23 | 24 | // // Fail if either date is invalid 25 | // for (let date of [startDate, endDate]){ 26 | // if (!date || isNaN(date)) 27 | // return null; 28 | // } 29 | 30 | return ( 31 |

37 | 40 | form.setFieldValue(`${field.name}.0`, format(date, DATE_FORMAT), true) 41 | } 42 | selectsStart 43 | startDate={startDate} 44 | endDate={endDate} 45 | customInput={} 46 | {...rest} 47 | /> 48 |  to  49 | 52 | form.setFieldValue(`${field.name}.1`, format(date, DATE_FORMAT), true) 53 | } 54 | selectsEnd 55 | startDate={startDate} 56 | endDate={endDate} 57 | minDate={startDate} 58 | customInput={} 59 | {...rest} 60 | /> 61 | {/**/} 62 | {/* {*/} 64 | {/* if (errors !== error) {*/} 65 | {/* form.setFieldError(field.name, error);*/} 66 | {/* }*/} 67 | {/* }}*/} 68 | {/* valid={valid}*/} 69 | {/* invalid={invalid}*/} 70 | {/* {...field}*/} 71 | {/* {...rest}*/} 72 | {/* onChange={date => {*/} 73 | {/* // Then, when the date is changed, we need to output in that same format*/} 74 | {/* form.setFieldValue(field.name, date.format(outputFormat), true);*/} 75 | {/* }}*/} 76 | {/* value={value}*/} 77 | {/* />*/} 78 | {/**/} 79 | 80 | {(msg) => {msg}} 81 | 82 |
83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /src/trend/bootstrapField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Input, FormFeedback } from "reactstrap"; 3 | import { ErrorMessage, getIn } from "formik"; 4 | 5 | /** 6 | * For use with Formik's Field 7 | */ 8 | export default function BootstrapField(props) { 9 | const { field, form, type, ...rest } = props; 10 | const touched = getIn(form.touched, field.name); 11 | const errors = getIn(form.errors, field.name); 12 | const invalid = touched && errors; 13 | const valid = touched && !errors; 14 | 15 | return ( 16 | <> 17 | 18 | 19 | {(msg) => {msg}} 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/trend/bootstrapHookField.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Input, FormFeedback } from "reactstrap"; 3 | import { ErrorMessage, getIn } from "formik"; 4 | 5 | /** 6 | * For use with Formik's useField() 7 | */ 8 | export default function BootstrapField({ input, meta, helper, ...rest }) { 9 | const invalid = meta.touched && meta.error; 10 | const valid = meta.touched && !meta.error; 11 | 12 | return ( 13 | <> 14 | 15 | 16 | {(msg) => {msg}} 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/trend/outlierDetection.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FormGroup, Label } from "reactstrap"; 3 | import BootstrapField from "./bootstrapField"; 4 | import * as Yup from "yup"; 5 | import BootstrapHookField from "./bootstrapHookField"; 6 | import AutoSave from "../util/autoSave"; 7 | 8 | import { Field, Formik, useField } from "formik"; 9 | 10 | /** 11 | * Converts a yup schema to a function that can be used to validate a formik field 12 | * @param yup 13 | */ 14 | function yupToMessage(yup) { 15 | return (value) => { 16 | return yup 17 | .validate(value) 18 | .then(() => { 19 | // If the promise succeeded, then the validation succeeded, so return no 20 | // error 21 | return undefined; 22 | }) 23 | .catch((err) => { 24 | // If the promise failed, then convert it into a message 25 | return err.join(","); 26 | }); 27 | }; 28 | } 29 | 30 | export default function OutlierDetection( 31 | outlierType = "outlierType", 32 | outlierThreshold = "outlierThreshold" 33 | ) { 34 | let thresholdName, showThreshold; 35 | const [typeField, typeMeta, typeHelpers] = useField({ 36 | name: outlierType, 37 | validate: yupToMessage( 38 | Yup.string().oneOf(["z", "none"]).label("Outlier Detection Method") 39 | ), 40 | type: "select", 41 | label: "Outlier Detection Method", 42 | }); 43 | const [thresholdField, thresholdMeta, thresholdHelpers] = useField({ 44 | name: outlierThreshold, 45 | validate: yupToMessage(Yup.number().label("Outlier Detection Threshold")), 46 | type: "number", 47 | label: "Outlier Detection Threshold", 48 | }); 49 | 50 | switch (typeField.value) { 51 | case "none": 52 | thresholdName = ""; 53 | showThreshold = false; 54 | break; 55 | case "grubbs": 56 | thresholdName = "Alpha value"; 57 | showThreshold = true; 58 | break; 59 | case "z": 60 | thresholdName = "Z-score"; 61 | showThreshold = true; 62 | break; 63 | default: 64 | showThreshold = false; 65 | thresholdName = ""; 66 | break; 67 | } 68 | 69 | return ( 70 | <> 71 | 72 | 73 | 74 | 75 | 81 | 82 | 83 | 84 | 85 | 86 | {showThreshold && ( 87 | 88 | 89 | 95 | 96 | )} 97 | 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /src/trend/plot.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { Button, Card, CardBody, CardHeader } from "reactstrap"; 3 | import Plot from "react-plotly.js"; 4 | import SavePlot from "./savePlot"; 5 | import PropTypes from "prop-types"; 6 | 7 | export default function TrendPlot({ 8 | currentUser, 9 | client, 10 | plotData, 11 | plotSettings, 12 | selectedFilter, 13 | }) { 14 | const [saveBoxOpen, openSaveBox] = useState(false); 15 | return ( 16 | 17 | { 28 | openSaveBox((open) => !open); 29 | }} 30 | /> 31 | 32 | 40 |

Trend Plot

41 |
42 | 43 | 56 | 57 |
58 | ); 59 | } 60 | TrendPlot.propTypes = { 61 | currentUser: PropTypes.object, 62 | client: PropTypes.object.isRequired, 63 | 64 | // The plot series 65 | plotData: PropTypes.object.isRequired, 66 | 67 | // The settings, used for saving the plot 68 | plotSettings: PropTypes.object.isRequired, 69 | selectedFilter: PropTypes.object.isRequired, 70 | }; 71 | -------------------------------------------------------------------------------- /src/trend/savePlot.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Button, 4 | Container, 5 | FormGroup, 6 | Label, 7 | Modal, 8 | ModalBody, 9 | ModalFooter, 10 | ModalHeader, 11 | Row, 12 | } from "reactstrap"; 13 | import BootstrapField from "./bootstrapField"; 14 | import filterSchema from "../util/filterSchema"; 15 | import * as Yup from "yup"; 16 | import PropTypes from "prop-types"; 17 | import { Field, Form, Formik } from "formik"; 18 | 19 | export default function SavePlot(props) { 20 | const { isOpen, toggle, qcApi, plotData, plotType, user } = props; 21 | 22 | return ( 23 | { 33 | // Note, this resource corresponds to SampleFilterSchema in the backend 34 | 35 | // Create the resource 36 | const resource = qcApi.create("favourites"); 37 | resource.set("title", values.title); 38 | resource.set("description", values.description); 39 | resource.set("plot_type", plotType); 40 | resource.set("data", plotData); 41 | 42 | resource.relationships("user").set(user); 43 | 44 | // Save it 45 | resource 46 | .sync() 47 | .then(() => { 48 | toggle(true); 49 | }) 50 | .finally(() => { 51 | setSubmitting(false); 52 | }); 53 | }} 54 | > 55 | {({ values, isSubmitting }) => ( 56 | toggle(false)}> 57 |
58 | 59 | Save as Favourite 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 86 | 87 |
88 |
89 | )} 90 |
91 | ); 92 | } 93 | 94 | SavePlot.propTypes = { 95 | isOpen: PropTypes.bool.isRequired, 96 | toggle: PropTypes.func.isRequired, 97 | qcApi: PropTypes.object.isRequired, 98 | plotData: PropTypes.object.isRequired, 99 | plotType: PropTypes.string.isRequired, 100 | user: PropTypes.object.isRequired, 101 | }; 102 | -------------------------------------------------------------------------------- /src/trend/trendSchema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "definitions": { 4 | "ControlLimitSchema": { 5 | "properties": { 6 | "alpha": { 7 | "title": "Alpha", 8 | "type": "number", 9 | "format": "Float" 10 | }, 11 | "enabled": { 12 | "title": "Enabled", 13 | "type": "boolean" 14 | } 15 | }, 16 | "type": "object", 17 | "additionalProperties": false 18 | }, 19 | "TrendInputSchema": { 20 | "properties": { 21 | "center_line": { 22 | "title": "Center Line", 23 | "type": "string", 24 | "description": "Type of center line", 25 | "enum": ["mean", "median", "none"], 26 | "enumNames": [] 27 | }, 28 | "control_limits": { 29 | "title": "Control Limits", 30 | "type": "object", 31 | "$ref": "#/definitions/ControlLimitSchema" 32 | }, 33 | "fields": { 34 | "type": "string", 35 | "title": "Fields" 36 | }, 37 | "filter": { 38 | "type": "array", 39 | "title": "Filter" 40 | }, 41 | "statistic": { 42 | "title": "Statistic", 43 | "type": "string", 44 | "default": "none", 45 | "description": "Which statistics are plotted. Measurement means unprocessed QC metrics, and Hotelling means a multivariate Hotelling or Mahalanobis distance.", 46 | "enum": ["measurement", "hotelling"], 47 | "enumNames": ["Measurement", "Hotelling"] 48 | } 49 | }, 50 | "type": "object", 51 | "required": ["center_line", "control_limits", "fields"], 52 | "additionalProperties": false 53 | } 54 | }, 55 | "$ref": "#/definitions/TrendInputSchema" 56 | } 57 | -------------------------------------------------------------------------------- /src/util/api.js: -------------------------------------------------------------------------------- 1 | import JsonApiClient from "@holidayextras/jsonapi-client"; 2 | 3 | /** 4 | * Returns an API client with an associated API key (synchronously). 5 | */ 6 | export function getClient(token) { 7 | let options = {}; 8 | 9 | // If we already have a token, attach it here 10 | if (token) { 11 | options = { 12 | header: { 13 | access_token: token, 14 | }, 15 | }; 16 | } 17 | 18 | // Construct the client 19 | return new JsonApiClient("/rest_api/v1", options); 20 | } 21 | 22 | /** 23 | * Returns a promise of an API client with an associated API key. 24 | */ 25 | export function getAuthenticatedClient(token) { 26 | const client = getClient(token); 27 | 28 | // If we don't have a token, we need to obtain one 29 | if (!token) { 30 | return getToken(client).then((token) => { 31 | client._transport._auth.header = { access_token: token }; 32 | return client; 33 | }); 34 | } else return Promise.resolve(client); 35 | } 36 | 37 | /** 38 | * Returns a promise that resolves to an API token 39 | */ 40 | export function getToken(client) { 41 | return client.get("users", "current").then((data) => { 42 | return data.toJSON().api_token; 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /src/util/autoSave.js: -------------------------------------------------------------------------------- 1 | import debounce from "just-debounce-it"; 2 | import React, { useState, useEffect, useCallback } from "react"; 3 | import { useFormikContext } from "formik"; 4 | 5 | export const AutoSave = ({ debounceMs }) => { 6 | const formik = useFormikContext(); 7 | const debouncedSubmit = useCallback(debounce(formik.submitForm, debounceMs), [ 8 | debounceMs, 9 | formik.submitForm, 10 | ]); 11 | 12 | useEffect(debouncedSubmit, [debouncedSubmit, formik.values]); 13 | 14 | return null; 15 | }; 16 | export default AutoSave; 17 | -------------------------------------------------------------------------------- /src/util/filter.js: -------------------------------------------------------------------------------- 1 | export default class Filter { 2 | constructor({ 3 | type = "samplemeta", 4 | key = "", 5 | comparison = "eq", 6 | value = [], 7 | } = {}) { 8 | this.type = type; 9 | this.key = key; 10 | this.cmp = comparison; 11 | this.value = value; 12 | } 13 | } 14 | 15 | export const DATE_FORMAT = "yyyy-MM-dd"; 16 | -------------------------------------------------------------------------------- /src/util/filterSchema.js: -------------------------------------------------------------------------------- 1 | import * as Yup from "yup"; 2 | 3 | const schema = Yup.object().shape({ 4 | filterName: Yup.string().min(3).label("Filter name").required(), 5 | filterGroup: Yup.string().label("Filter group").required(), 6 | visibility: Yup.string() 7 | .oneOf(["private", "public"]) 8 | .label("Visibility") 9 | .required(), 10 | filters: Yup.array().of( 11 | Yup.array() 12 | .strict() 13 | .of( 14 | Yup.object().shape({ 15 | type: Yup.string() 16 | .oneOf(["timedelta", "daterange", "reportmeta", "samplemeta"]) 17 | .label("Type") 18 | .required(), 19 | key: Yup.mixed() 20 | .label("Key") 21 | .when("type", { 22 | is: (val) => ["samplemeta", "reportmeta"].includes(val), 23 | then: Yup.string().required(), 24 | otherwise: Yup.mixed().notRequired(), 25 | }), 26 | cmp: Yup.string() 27 | .label("Comparison") 28 | .required() 29 | .when("type", { 30 | is: (val) => ["samplemeta", "reportmeta"].includes(val), 31 | then: Yup.string().oneOf([ 32 | "eq", 33 | "ne", 34 | "le", 35 | "lt", 36 | "ge", 37 | "gt", 38 | "contains", 39 | "like", 40 | "startswith", 41 | "endswith", 42 | "notcontains", 43 | "notlike", 44 | "notstartswith", 45 | "notendswith", 46 | ]), 47 | otherwise: Yup.string().oneOf(["in", "not in"]), 48 | }), 49 | value: Yup.array().label("Value").required(), 50 | }) 51 | ) 52 | ), 53 | }); 54 | export default schema; 55 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env sh 2 | set -e 3 | 4 | # If there's a prestart.sh script in the /app directory, run it before starting 5 | PRE_START_PATH=/app/prestart.sh 6 | echo "Checking for script in $PRE_START_PATH" 7 | if [ -f $PRE_START_PATH ] ; then 8 | echo "Running script $PRE_START_PATH" 9 | . "$PRE_START_PATH" 10 | else 11 | echo "There is no script $PRE_START_PATH" 12 | fi 13 | 14 | # Start Gunicorn 15 | exec gunicorn -c "$GUNICORN_CONF" "$APP_MODULE" -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests for the app. 3 | """ 4 | -------------------------------------------------------------------------------- /tests/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiQC/MegaQC/d1c2ea3253324093fdbc02895c2ee96414955774/tests/api/__init__.py -------------------------------------------------------------------------------- /tests/api/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from flask import request 3 | from flask.testing import FlaskClient 4 | 5 | from tests import factories 6 | 7 | 8 | @pytest.fixture(scope="function") 9 | def client(app): 10 | with app.test_client() as c: 11 | c: FlaskClient 12 | yield c 13 | 14 | 15 | @pytest.fixture(scope="function") 16 | def token(db: str): 17 | user = factories.UserFactory(is_admin=False) 18 | db.session.add(user) 19 | db.session.commit() 20 | return user.api_token 21 | 22 | 23 | @pytest.fixture(scope="function") 24 | def admin_token(db) -> str: 25 | user = factories.UserFactory(is_admin=True) 26 | db.session.add(user) 27 | db.session.commit() 28 | return user.api_token 29 | -------------------------------------------------------------------------------- /tests/api/test_current_user.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from flask_login import login_user 3 | from pkg_resources import resource_stream 4 | 5 | from megaqc.model import models 6 | from megaqc.rest_api import schemas 7 | from tests import factories 8 | 9 | 10 | def test_current_user_session_working(session, token, app): 11 | """ 12 | Test the current_user endpoint, using a valid session. 13 | 14 | This should work 15 | """ 16 | # Create a user 17 | user = factories.UserFactory() 18 | session.add(user) 19 | session.commit() 20 | 21 | # Login with that user (we have to use a custom client here) 22 | with app.test_client(user=user) as client: 23 | rv = client.get("/rest_api/v1/users/current") 24 | 25 | # Check the request was successful 26 | assert rv.status_code == 200, rv.json 27 | 28 | # Validate the response 29 | schemas.UserSchema().validate(rv.json) 30 | 31 | 32 | def test_current_user_session_invalid(session, client, token): 33 | """ 34 | Test the current_user endpoint, using a valid session. 35 | 36 | This should work 37 | """ 38 | # Create a user 39 | user = factories.UserFactory() 40 | session.add(user) 41 | session.commit() 42 | 43 | rv = client.get("/rest_api/v1/users/current") 44 | 45 | # Check the request was unauthorized 46 | assert rv.status_code == 401 47 | -------------------------------------------------------------------------------- /tests/api/test_filter.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pkg_resources import resource_stream 3 | 4 | from megaqc.model import models 5 | from megaqc.rest_api import schemas 6 | from tests import factories 7 | 8 | 9 | @pytest.fixture() 10 | def report(session): 11 | r = factories.ReportFactory() 12 | session.add(r) 13 | session.commit() 14 | return r 15 | -------------------------------------------------------------------------------- /tests/api/test_plot.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from flask import url_for 4 | from marshmallow.utils import EXCLUDE 5 | from plotly.offline import plot 6 | 7 | from megaqc.rest_api.schemas import TrendSchema 8 | from tests import factories 9 | 10 | 11 | def test_trend_data_measurement(db, client): 12 | # Create 5 reports each with 1 sample. Each has a single field called 'test_field' 13 | data_type = factories.SampleDataTypeFactory() 14 | report = factories.ReportFactory.create_batch(5, samples__data__data_type=data_type) 15 | db.session.add_all(report) 16 | db.session.commit() 17 | 18 | url = url_for( 19 | "rest_api.trend_data", 20 | **{ 21 | "filter": json.dumps([]), 22 | "fields": json.dumps([data_type.data_key]), 23 | "statistic": "measurement", 24 | "statistic_options[center_line]": "mean", 25 | }, 26 | ) 27 | response = client.get(url, headers={"Content-Type": "application/json"}) 28 | 29 | # Check the request was successful 30 | assert ( 31 | response.status_code == 200 32 | ), f"Status code {response.status_code}, full response: {response.json}" 33 | 34 | # unknown=EXCLUDE ensures we don't keep the ID field when we load at this point 35 | data = TrendSchema(many=True, unknown=EXCLUDE).load(response.json) 36 | 37 | # Check that there are 2 series (mean, raw data) 38 | assert len(data) == 2 39 | 40 | # Test that this is valid plot data 41 | plot({"data": data}, validate=True, auto_open=False) 42 | 43 | 44 | def test_trend_data_iforest(db, client): 45 | # Create 5 reports each with 1 sample. Each has a single field called 'test_field' 46 | data_type = factories.SampleDataTypeFactory() 47 | report = factories.ReportFactory.create_batch(5, samples__data__data_type=data_type) 48 | db.session.add_all(report) 49 | db.session.commit() 50 | 51 | url = url_for( 52 | "rest_api.trend_data", 53 | **{ 54 | "filter": json.dumps([]), 55 | "fields": json.dumps([data_type.data_key]), 56 | "statistic": "iforest", 57 | "statistic_options[contamination]": "0.01", 58 | }, 59 | ) 60 | response = client.get(url, headers={"Content-Type": "application/json"}) 61 | 62 | # Check the request was successful 63 | assert ( 64 | response.status_code == 200 65 | ), f"Status code {response.status_code}, full response: {response.json}" 66 | 67 | # unknown=EXCLUDE ensures we don't keep the ID field when we load at this point 68 | data = TrendSchema(many=True, unknown=EXCLUDE).load(response.json) 69 | 70 | # Check that there are 3 series (mean, inlier, outlier) 71 | assert len(data) == 2 72 | 73 | # Test that this is valid plot data 74 | plot({"data": data}, validate=True, auto_open=False) 75 | -------------------------------------------------------------------------------- /tests/api/test_upload.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from pkg_resources import resource_stream 3 | 4 | from megaqc.model import models 5 | from megaqc.rest_api import schemas 6 | from tests import factories 7 | 8 | 9 | @pytest.fixture() 10 | def upload(session): 11 | r = factories.UploadFactory() 12 | session.add(r) 13 | session.commit() 14 | return r 15 | 16 | 17 | def test_post_upload_list(db, client, token): 18 | """ 19 | Test uploading a report. 20 | """ 21 | count_1 = db.session.query(models.Upload).count() 22 | 23 | rv = client.post( 24 | "/rest_api/v1/uploads", 25 | data={"report": resource_stream("tests", "multiqc_data.json")}, 26 | headers={ 27 | "access_token": token, 28 | "Content-Type": "multipart/form-data", 29 | "Accept": "application/json", 30 | }, 31 | ) 32 | 33 | # Check the request was successful 34 | assert rv.status_code == 201, rv.json 35 | 36 | # Validate the response 37 | schemas.UploadSchema().validate(rv.json) 38 | 39 | # Check that there is a new Upload 40 | count_2 = db.session.query(models.Upload).count() 41 | assert count_2 == count_1 + 1 42 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Defines fixtures available to all tests. 4 | """ 5 | 6 | from pathlib import Path 7 | 8 | import pytest 9 | from sqlalchemy.orm.session import Session 10 | from webtest import TestApp 11 | 12 | from megaqc.app import create_app 13 | from megaqc.database import db as _db 14 | from megaqc.database import init_db 15 | from megaqc.settings import TestConfig 16 | 17 | from .factories import UserFactory 18 | 19 | 20 | @pytest.fixture(scope="function") 21 | def multiqc_data(): 22 | here = Path(__file__).parent 23 | with (here / "multiqc_data.json").open() as fp: 24 | return fp.read() 25 | 26 | 27 | @pytest.fixture(scope="function") 28 | def app(): 29 | """ 30 | An application for the tests. 31 | """ 32 | config = TestConfig() 33 | _app = create_app(config) 34 | ctx = _app.test_request_context() 35 | ctx.push() 36 | init_db(config.SQLALCHEMY_DATABASE_URI) 37 | 38 | yield _app 39 | 40 | ctx.pop() 41 | 42 | 43 | @pytest.fixture(scope="function") 44 | def testapp(app): 45 | """ 46 | A Webtest app. 47 | """ 48 | return TestApp(app) 49 | 50 | 51 | @pytest.fixture(scope="function") 52 | def db(app): 53 | """ 54 | A database for the tests. 55 | """ 56 | _db.app = app 57 | with app.app_context(): 58 | _db.create_all() 59 | 60 | yield _db 61 | 62 | # Explicitly close DB connection 63 | _db.session.close() 64 | _db.drop_all() 65 | 66 | 67 | @pytest.fixture 68 | def user(db): 69 | """ 70 | A user for the tests. 71 | """ 72 | user = UserFactory(password="myprecious") 73 | db.session.commit() 74 | return user 75 | 76 | 77 | @pytest.fixture() 78 | def session(db): 79 | sess: Session = db.session 80 | return sess 81 | -------------------------------------------------------------------------------- /tests/test_config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Test configs. 4 | """ 5 | import pytest 6 | 7 | from megaqc.app import create_app 8 | from megaqc.settings import DevConfig, ProdConfig 9 | 10 | 11 | @pytest.mark.noautofixt 12 | def test_production_config(): 13 | """ 14 | Production config. 15 | """ 16 | app = create_app(ProdConfig) 17 | assert app.config["ENV"] == "prod" 18 | assert app.config["DEBUG"] is False 19 | assert app.config["DEBUG_TB_ENABLED"] is False 20 | 21 | 22 | @pytest.mark.noautofixt 23 | def test_dev_config(): 24 | """ 25 | Development config. 26 | """ 27 | app = create_app(DevConfig) 28 | assert app.config["ENV"] == "dev" 29 | assert app.config["DEBUG"] is True 30 | -------------------------------------------------------------------------------- /tests/test_docker.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import time 3 | from pathlib import Path 4 | 5 | import pytest 6 | import requests 7 | 8 | 9 | def raise_response(resp): 10 | """ 11 | :param resp: Requests response object. 12 | """ 13 | if not str(resp.status_code).startswith("2"): 14 | raise Exception( 15 | "Request failed with status {} and body {}".format( 16 | resp.status_code, resp.text 17 | ) 18 | ) 19 | 20 | 21 | @pytest.fixture(scope="module") 22 | def compose_stack(): 23 | deploy = (Path(__file__).parent.parent / "deployment").resolve() 24 | # Start the stack, and wait for it to start up 25 | subprocess.run( 26 | ["docker-compose", "up", "--build", "--detach"], cwd=deploy, check=True 27 | ) 28 | time.sleep(60) 29 | yield 30 | # When we're done, stop the stack and cleanup the volumes 31 | subprocess.run(["docker-compose", "down", "-v"], cwd=deploy, check=True) 32 | 33 | 34 | def test_docker(): 35 | root = (Path(__file__).parent.parent).resolve() 36 | subprocess.run(["docker", "build", str(root)], check=True) 37 | 38 | 39 | def test_compose(multiqc_data, compose_stack): 40 | # Create a user 41 | user = requests.post( 42 | "http://localhost/rest_api/v1/users", 43 | json={ 44 | "data": { 45 | "type": "users", 46 | "attributes": { 47 | "username": "foo", 48 | "email": "foo@bar.com", 49 | "password": "bar", 50 | }, 51 | } 52 | }, 53 | ) 54 | raise_response(user) 55 | 56 | user.raise_for_status() 57 | token = user.json()["data"]["attributes"]["api_token"] 58 | 59 | # Upload the report 60 | report = requests.post( 61 | url="http://localhost/rest_api/v1/uploads", 62 | files={"report": multiqc_data}, 63 | headers={"access_token": token}, 64 | ) 65 | raise_response(report) 66 | report.raise_for_status() 67 | 68 | # Finally, we should have 1 report 69 | result = requests.get( 70 | url="http://localhost/rest_api/v1/uploads", 71 | headers={"access_token": token}, 72 | ) 73 | raise_response(result) 74 | assert len(result.json()["data"]) == 1 75 | 76 | 77 | def test_https_redirects(compose_stack): 78 | """ 79 | Test that all redirects use https. 80 | """ 81 | # This redirects even if you aren't logged in 82 | result = requests.get(url="https://localhost/logout/", verify=False) 83 | 84 | # If the url we ended up at is an https URL then it's redirected correctly 85 | assert result.url.startswith("https://") 86 | -------------------------------------------------------------------------------- /tests/test_forms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Test forms. 4 | """ 5 | import pytest 6 | 7 | from megaqc.public.forms import LoginForm 8 | from megaqc.user.forms import RegisterForm 9 | from tests.factories import UserFactory 10 | 11 | 12 | @pytest.fixture() 13 | def user_attrs(): 14 | return UserFactory.build() 15 | 16 | 17 | class TestRegisterForm: 18 | """ 19 | Register form. 20 | """ 21 | 22 | def test_validate_user_already_registered(self, user, user_attrs): 23 | """ 24 | Enter username that is already registered. 25 | """ 26 | form = RegisterForm( 27 | username=user.username, 28 | email=user_attrs.email, 29 | first_name=user_attrs.first_name, 30 | last_name=user_attrs.last_name, 31 | password="password", 32 | confirm="password", 33 | ) 34 | 35 | assert form.validate() is False 36 | assert "Username already registered" in form.username.errors 37 | 38 | def test_validate_email_already_registered(self, user, user_attrs): 39 | """ 40 | Enter email that is already registered. 41 | """ 42 | form = RegisterForm( 43 | username=user_attrs.username, 44 | email=user.email, 45 | first_name=user_attrs.first_name, 46 | last_name=user_attrs.last_name, 47 | password="password", 48 | confirm="password", 49 | ) 50 | 51 | assert form.validate() is False 52 | assert "Email already registered" in form.email.errors 53 | 54 | def test_validate_success(self, user_attrs, app): 55 | """ 56 | Register with success. 57 | """ 58 | form = RegisterForm( 59 | username=user_attrs.username, 60 | email=user_attrs.email, 61 | first_name=user_attrs.first_name, 62 | last_name=user_attrs.last_name, 63 | password="password", 64 | confirm="password", 65 | ) 66 | assert form.validate() is True 67 | 68 | 69 | class TestLoginForm: 70 | """ 71 | Login form. 72 | """ 73 | 74 | def test_validate_success(self, user): 75 | """ 76 | Login successful. 77 | """ 78 | user.set_password("example") 79 | user.save() 80 | form = LoginForm(username=user.username, password="example") 81 | assert form.validate() is True 82 | assert form.user == user 83 | 84 | def test_validate_unknown_username(self, db): 85 | """ 86 | Unknown username. 87 | """ 88 | form = LoginForm(username="unknown", password="example") 89 | assert form.validate() is False 90 | assert "Unknown username" in form.username.errors 91 | assert form.user is None 92 | 93 | def test_validate_invalid_password(self, user): 94 | """ 95 | Invalid password. 96 | """ 97 | user.set_password("example") 98 | user.save() 99 | form = LoginForm(username=user.username, password="wrongpassword") 100 | assert form.validate() is False 101 | assert "Invalid password" in form.password.errors 102 | 103 | def test_validate_inactive_user(self, user): 104 | """ 105 | Inactive user. 106 | """ 107 | user.active = False 108 | user.set_password("example") 109 | user.save() 110 | # Correct username and password, but user is not activated 111 | form = LoginForm(username=user.username, password="example") 112 | assert form.validate() is False 113 | assert "User not activated" in form.username.errors 114 | -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Model unit tests. 4 | """ 5 | import datetime as dt 6 | 7 | import pytest 8 | 9 | from megaqc.user.models import Role, User 10 | 11 | from .factories import UserFactory 12 | 13 | 14 | @pytest.mark.usefixtures("db") 15 | class TestUser: 16 | """ 17 | User tests. 18 | """ 19 | 20 | def test_created_at_defaults_to_datetime(self): 21 | """ 22 | Test creation date. 23 | """ 24 | user = User(username="foo", email="foo@bar.com") 25 | user.save() 26 | assert bool(user.created_at) 27 | assert isinstance(user.created_at, dt.datetime) 28 | 29 | def test_password_is_nullable(self): 30 | """ 31 | Test null password. 32 | """ 33 | user = User(username="foo", email="foo@bar.com") 34 | user.save() 35 | assert user.password is None 36 | 37 | def test_factory(self, db): 38 | """ 39 | Test user factory. 40 | """ 41 | user = UserFactory(password="myprecious") 42 | db.session.commit() 43 | assert bool(user.username) 44 | assert bool(user.email) 45 | assert bool(user.created_at) 46 | assert user.is_admin is False 47 | assert user.active is True 48 | assert user.check_password("myprecious") 49 | 50 | def test_check_password(self): 51 | """ 52 | Check password. 53 | """ 54 | user = User.create(username="foo", email="foo@bar.com", password="foobarbaz123") 55 | assert user.check_password("foobarbaz123") is True 56 | assert user.check_password("barfoobaz") is False 57 | 58 | def test_full_name(self): 59 | """ 60 | User full name. 61 | """ 62 | user = UserFactory(first_name="Foo", last_name="Bar") 63 | assert user.full_name == "Foo Bar" 64 | 65 | def test_roles(self): 66 | """ 67 | Add a role to a user. 68 | """ 69 | role = Role(name="admin") 70 | role.save() 71 | user = UserFactory() 72 | user.roles.append(role) 73 | user.save() 74 | assert role in user.roles 75 | -------------------------------------------------------------------------------- /uploads/README.md: -------------------------------------------------------------------------------- 1 | # MegaQC Uploads folder 2 | 3 | This folder contains temporary MultiQC report data (gzipped JSON files). 4 | 5 | As the process of saving data can take quite a long time, we don't want 6 | to make the MultiQC user wait. Instead, we just save the file here and 7 | continue with MultiQC execution. MegaQC then processes these files 8 | asynchronously with a scheduled process. 9 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const path = require("path"); 3 | 4 | // const {DuplicatesPlugin} = require("inspectpack/plugin"); 5 | // const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 6 | 7 | module.exports = { 8 | entry: { 9 | // This allows for multiple React "apps", for different pages 10 | trend: "./src/trend.js", 11 | admin: "./src/admin.js", 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.(js|jsx)$/, 17 | exclude: /node_modules/, 18 | use: [ 19 | { 20 | options: { 21 | presets: ["@babel/preset-env", "@babel/preset-react"], 22 | plugins: [ 23 | "@babel/plugin-proposal-object-rest-spread", 24 | "@babel/plugin-transform-runtime", 25 | ], 26 | }, 27 | loader: "babel-loader", 28 | }, 29 | ], 30 | }, 31 | { 32 | test: /\.css$/i, 33 | use: ["style-loader", "css-loader"], 34 | }, 35 | { 36 | test: /\.svg$/, 37 | use: ["url-loader"], 38 | }, 39 | ], 40 | }, 41 | resolve: { 42 | alias: { 43 | react: path.resolve("./node_modules/react"), 44 | }, 45 | extensions: ["*", ".js", ".jsx"], 46 | }, 47 | output: { 48 | path: __dirname + "/megaqc/static/js", 49 | publicPath: "/", 50 | filename: "[name].js", 51 | }, 52 | plugins: [], 53 | devServer: { 54 | contentBase: "./dist", 55 | hot: true, 56 | }, 57 | devtool: "source-map", 58 | }; 59 | --------------------------------------------------------------------------------