├── .coveragerc ├── .gitattributes ├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── main.yml │ └── sp-deployment-pipeline.yml ├── .gitignore ├── .pep8speaks.yml ├── .pycodestylerc ├── .pydocstylerc ├── LICENSE ├── README.md ├── bootstrap_gunicorn.py ├── config_parser.py ├── config_sample.py ├── database.py ├── decorators.py ├── exceptions.py ├── install ├── __init__.py ├── ci-vm │ ├── ci-linux │ │ ├── ci │ │ │ ├── bootstrap │ │ │ ├── runCI │ │ │ └── variables │ │ └── startup-script.sh │ ├── ci-windows │ │ ├── ci │ │ │ ├── runCI.bat │ │ │ └── variables.bat │ │ ├── rclone_sample.conf │ │ └── startup-script.ps1 │ └── installation.md ├── init_db.py ├── install.sh ├── installation.md ├── nginx.conf ├── platform ├── sample_db.py └── sample_files │ ├── sample1.ts │ └── sample2.ts ├── log_configuration.py ├── logs └── .keep ├── mailer.py ├── manage.py ├── migrations ├── README ├── alembic.ini ├── env.py ├── script.py.mako └── versions │ ├── 2e0d2e02a721_.py │ ├── 6b1274f61edd_.py │ ├── 6b335fbd58ab_.py │ ├── a5183973c3e9_.py │ └── b3ed927671bd_.py ├── mod_auth ├── __init__.py ├── controllers.py ├── forms.py └── models.py ├── mod_ci ├── __init__.py ├── controllers.py ├── cron.py ├── forms.py └── models.py ├── mod_customized ├── __init__.py ├── controllers.py ├── forms.py └── models.py ├── mod_home ├── __init__.py ├── controllers.py └── models.py ├── mod_regression ├── __init__.py ├── controllers.py ├── forms.py ├── models.py └── update_regression.py ├── mod_sample ├── __init__.py ├── controllers.py ├── forms.py ├── media_info_parser.py └── models.py ├── mod_test ├── __init__.py ├── controllers.py ├── models.py └── nicediff │ ├── __init__.py │ └── diff.py ├── mod_upload ├── __init__.py ├── controllers.py ├── forms.py ├── models.py └── progress_ftp_upload.py ├── mypy.ini ├── requirements.txt ├── run.py ├── static ├── browserconfig.xml ├── css │ ├── app.css │ ├── font-awesome.css │ ├── font-awesome.min.css │ ├── foundation-dark.css │ ├── foundation-dark.min.css │ ├── foundation-light.css │ └── foundation-light.min.css ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ └── fontawesome-webfont.woff2 ├── img │ ├── favicon │ │ ├── android-chrome-144x144.png │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-36x36.png │ │ ├── android-chrome-48x48.png │ │ ├── android-chrome-72x72.png │ │ ├── android-chrome-96x96.png │ │ ├── apple-touch-icon-114x114.png │ │ ├── apple-touch-icon-120x120.png │ │ ├── apple-touch-icon-144x144.png │ │ ├── apple-touch-icon-152x152.png │ │ ├── apple-touch-icon-180x180.png │ │ ├── apple-touch-icon-57x57.png │ │ ├── apple-touch-icon-60x60.png │ │ ├── apple-touch-icon-72x72.png │ │ ├── apple-touch-icon-76x76.png │ │ ├── apple-touch-icon-precomposed.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── favicon.ico │ │ ├── mstile-144x144.png │ │ ├── mstile-150x150.png │ │ ├── mstile-310x150.png │ │ ├── mstile-310x310.png │ │ ├── mstile-70x70.png │ │ └── safari-pinned-tab.svg │ ├── filezilla.png │ └── status │ │ └── .keep ├── js │ ├── app.js │ ├── same-blocks-highlight.js │ ├── sorttable.js │ ├── theme.js │ └── vendor │ │ ├── foundation.js │ │ ├── foundation.min.js │ │ ├── jquery.js │ │ └── what-input.js ├── manifest.json └── svg │ ├── .keep │ ├── ERROR-linux.svg │ ├── ERROR-windows.svg │ ├── FAILURE-linux.svg │ ├── FAILURE-windows.svg │ ├── SUCCESS-linux.svg │ └── SUCCESS-windows.svg ├── templates ├── 400.html ├── 403.html ├── 404.html ├── 500.html ├── auth │ ├── complete_reset.html │ ├── complete_signup.html │ ├── deactivate.html │ ├── login.html │ ├── manage.html │ ├── reset.html │ ├── reset_user.html │ ├── role.html │ ├── signup.html │ ├── user.html │ └── users.html ├── base.html ├── ci │ ├── blocked_users.html │ ├── blocked_users_remove.html │ ├── maintenance.html │ └── pr_comment.txt ├── custom │ └── index.html ├── email │ ├── email_changed.txt │ ├── new_issue.txt │ ├── password_changed.txt │ ├── password_reset.txt │ ├── recovery_link.txt │ ├── registration_email.txt │ ├── registration_existing.txt │ └── registration_ok.txt ├── home │ ├── about.html │ └── index.html ├── macros.html ├── menu.html ├── regression │ ├── by_sample.html │ ├── category_add.html │ ├── category_delete.html │ ├── category_edit.html │ ├── index.html │ ├── output_add.html │ ├── output_remove.html │ ├── test_add.html │ ├── test_delete.html │ ├── test_edit.html │ └── test_view.html ├── sample │ ├── delete_sample.html │ ├── delete_sample_additional.html │ ├── edit_sample.html │ ├── index.html │ ├── list_issues.html │ ├── list_samples.html │ ├── sample_info.html │ └── sample_not_found.html ├── test │ ├── by_id.html │ ├── index.html │ ├── list_table.html │ └── test_not_found.html └── upload │ ├── delete_id.html │ ├── filezilla_template.xml │ ├── ftp_index.html │ ├── index.html │ ├── index_admin.html │ ├── link_id.html │ ├── process_id.html │ ├── process_table.html │ ├── queued_sample_not_found.html │ └── upload.html ├── test-requirements.txt ├── tests ├── __init__.py ├── base.py ├── static_mock_files │ ├── expected.txt │ └── obtained.txt ├── test_auth │ ├── __init__.py │ ├── test_controllers.py │ └── test_forms.py ├── test_ci │ ├── __init__.py │ ├── test_controllers.py │ └── test_markdown.py ├── test_config_parser.py ├── test_customized │ ├── __init__.py │ └── test_controllers.py ├── test_home │ ├── __init__.py │ └── test_controllers.py ├── test_log_configuration.py ├── test_mailer.py ├── test_regression │ ├── __init__.py │ ├── test_controllers.py │ └── test_update_regression.py ├── test_run.py ├── test_sample │ ├── __init__.py │ ├── test_controllers.py │ └── test_media_info_parser.py ├── test_test │ ├── __init__.py │ ├── test_controllers.py │ └── test_diff.py ├── test_upload │ ├── __init__.py │ ├── test_controllers.py │ └── test_progress_FTP_upload.py └── test_utility.py ├── unittest.cfg └── utility.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | omit = 4 | install/* 5 | tests/* 6 | *__init__* 7 | config_sample.py 8 | bootstrap_gunicorn.py 9 | manage.py 10 | 11 | [report] 12 | exclude_lines = 13 | # Have to re-enable the standard pragma 14 | pragma: no cover 15 | 16 | # Don't complain if tests don't hit defensive assertion code: 17 | raise NotImplementedError 18 | pass 19 | # Ignore main code 20 | if __name__ == .__main__.: 21 | # Ignore the __repr__ function since it's nowhere used other than for represantation 22 | def __repr__ 23 | 24 | [html] 25 | directory = coverage_report 26 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.bat text eol=crlf -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @canihavesomecoffee @thealphadollar 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributors Guide 2 | 3 | Please read and understand the contribution guide before creating an issue or pull request. 4 | 5 | ## Etiquette 6 | 7 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code held within. They make the code freely available in the hope that it will be of use to other developers. It would be extremely unfair for them to suffer abuse or anger for their hard work. 8 | 9 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the world that developers are civilized and selfless people. 10 | 11 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 12 | 13 | ## Viability 14 | 15 | When requesting or submitting new features, first consider whether it might be useful to others. Open source projects are used by many developers, who may have entirely different needs to your own. Think about whether or not your feature is likely to be used by other users of the project. 16 | 17 | ## Procedure 18 | 19 | Before filing an issue: 20 | 21 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 22 | - Check to make sure your feature suggestion isn't already present within the project. 23 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 24 | - Check the pull requests tab to ensure that the feature isn't already in progress. 25 | 26 | Before submitting a pull request: 27 | 28 | - Ensure that your submission is [viable](#viability) for the project. 29 | - Check the codebase to ensure that your feature doesn't already exist. 30 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 31 | 32 | ## Requirements for a pull request 33 | 34 | - Please make sure you conform to the [PEP-8 coding guidelines](https://www.python.org/dev/peps/pep-0008/) for code 35 | - For docstrings, please adhere to [PEP-257](https://www.python.org/dev/peps/pep-0257/) 36 | - For typing, please adhere to [PEP-484](https://www.python.org/dev/peps/pep-0484/) 37 | 38 | A code checker for PEP-8 has been enabled. It is recommended to check your code and run tests before making a PR, and it's likewise recommended to use an IDE to check your code ([PyCharm](https://www.jetbrains.com/pycharm/) offers a free community edition, and has student licenses as well!) 39 | 40 | ## Credits 41 | 42 | This contributors guide is borrowed from https://github.com/nishad/udemy-dl/blob/master/.github/CONTRIBUTING.md, so thanks for that! -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please prefix your issue with one of the following: [BUG], [PROPOSAL], [QUESTION]. 2 | 3 | Sample platform commit (found at the bottom of each page) : **X.X.X** 4 | 5 | **In raising this issue, I confirm the following (please check boxes, eg [X]):** 6 | 7 | - [ ] I have read and understood the [contributors guide](https://github.com/CCExtractor/sample-platform/blob/master/.github/CONTRIBUTING.md). 8 | - [ ] I have checked that the bug-fix I am reporting can be replicated, or that the feature I am suggesting isn't already present. 9 | - [ ] I have checked that the issue I'm posting isn't already reported. 10 | - [ ] I have checked that the issue I'm posting isn't already solved and no duplicates exist in [closed issues](https://github.com/CCExtractor/sample-platform/issues?q=is%3Aissue+is%3Aclosed) and in [opened issues](https://github.com/CCExtractor/sample-platform/issues) 11 | - [ ] I have checked the pull requests tab for existing solutions/implementations to my issue/suggestion. 12 | 13 | **My familiarity with the project is as follows (check one, eg [X]):** 14 | 15 | - [ ] I have never visited/used the platform. 16 | - [ ] I have used the platform just a couple of times. 17 | - [ ] I have used the platform extensively, but have not contributed previously. 18 | - [ ] I am an active contributor to the platform. 19 | 20 | --- 21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Please prefix your pull request with one of the following: **[FEATURE]** **[FIX]** **[IMPROVEMENT]**. 2 | 3 | **In raising this pull request, I confirm the following (please check boxes):** 4 | 5 | - [ ] I have read and understood the [contributors guide](https://github.com/CCExtractor/sample-platform/blob/master/.github/CONTRIBUTING.md). 6 | - [ ] I have checked that another pull request for this purpose does not exist. 7 | - [ ] I have considered, and confirmed that this submission will be valuable to others. 8 | - [ ] I accept that this submission may not be used, and the pull request closed at the will of the maintainer. 9 | - [ ] I give this submission freely, and claim no ownership to its content. 10 | 11 | **My familiarity with the project is as follows (check one):** 12 | 13 | - [ ] I have never used the project. 14 | - [ ] I have used the project briefly. 15 | - [ ] I have used the project extensively, but have not contributed previously. 16 | - [ ] I am an active contributor to the project. 17 | 18 | --- 19 | 20 | {pull request content here} 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | timezone: America/Los_Angeles 9 | open-pull-requests-limit: 10 10 | reviewers: 11 | - canihavesomecoffee 12 | - thealphadollar 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Run tests and code checks 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | 10 | env: 11 | TESTING: true 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | python-version: ['3.8', '3.9', '3.10', '3.11'] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | sudo apt-get update 30 | sudo apt-get install libvirt-dev 31 | python -m pip install --upgrade pip 32 | if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi 33 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 34 | - name: Apply dodgy 35 | run: | 36 | dodgy 37 | - name: Apply isort 38 | run: | 39 | isort . --check-only 40 | - name: Apply pydocstyle 41 | run: | 42 | pydocstyle --config=./.pydocstylerc 43 | - name: Apply mypy 44 | run: | 45 | mypy --install-types --non-interactive . 46 | - name: Apply pycodestyle 47 | run: | 48 | pycodestyle ./ --config=./.pycodestylerc 49 | - name: Test with nose 50 | run: | 51 | nose2 52 | - name: Upload to codecov 53 | uses: codecov/codecov-action@v3 54 | with: 55 | flags: unittests 56 | name: sample-platform 57 | fail_ci_if_error: true 58 | files: ./coverage.xml 59 | token: ${{ secrets.CODECOV_TOKEN }} 60 | -------------------------------------------------------------------------------- /.github/workflows/sp-deployment-pipeline.yml: -------------------------------------------------------------------------------- 1 | name: Deploy sample platform 2 | 3 | on: 4 | workflow_dispatch: 5 | workflow_run: 6 | workflows: [ "Run tests and code checks" ] 7 | types: [ completed ] 8 | branches: 9 | - "master" 10 | 11 | env: 12 | DEPLOY_BRANCH: master 13 | 14 | jobs: 15 | deploy: 16 | runs-on: ubuntu-latest 17 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 18 | permissions: 19 | id-token: write 20 | contents: read # required for actions/checkout 21 | steps: 22 | - name: Deployment with ssh commands using ssh key 23 | uses: appleboy/ssh-action@master 24 | with: 25 | host: ${{ vars.PLATFORM_DOMAIN }} 26 | username: ${{ vars.SSH_USER }} 27 | key: ${{ secrets.SSH_KEY_PRIVATE }} 28 | port: 22 29 | script_stop: true 30 | command_timeout: 10m 31 | script: | 32 | echo "defining directories" 33 | INSTALL_FOLDER="/var/www/sample-platform" 34 | SAMPLE_REPOSITORY="/repository" 35 | 36 | echo "jump to app folder" 37 | cd $INSTALL_FOLDER 38 | 39 | echo "checkout branch" 40 | sudo git restore . 41 | sudo git checkout ${{env.DEPLOY_BRANCH}} 42 | sudo git fetch origin ${{env.DEPLOY_BRANCH}} 43 | 44 | echo "avoid merge conflicts" 45 | sudo git reset --hard origin/${{env.DEPLOY_BRANCH}} 46 | sudo git clean -f -d 47 | 48 | echo "update app from git" 49 | sudo git pull origin ${{env.DEPLOY_BRANCH}} 50 | 51 | echo "update dependencies" 52 | sudo python -m pip install -r requirements.txt 53 | 54 | echo "run migrations" 55 | sudo FLASK_APP=./run.py flask db upgrade 56 | 57 | echo "update runCI script files" 58 | sudo cp "install/ci-vm/ci-linux/ci/bootstrap" "${SAMPLE_REPOSITORY}/TestData/ci-linux/bootstrap" 59 | sudo cp "install/ci-vm/ci-linux/ci/runCI" "${SAMPLE_REPOSITORY}/TestData/ci-linux/runCI" 60 | sudo cp "install/ci-vm/ci-windows/ci/runCI.bat" "${SAMPLE_REPOSITORY}/TestData/ci-windows/runCI.bat" 61 | 62 | echo "reload server" 63 | sudo systemctl reload platform 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff: 2 | .idea/workspace.xml 3 | .idea/tasks.xml 4 | .idea/dictionaries 5 | .idea/vcs.xml 6 | .idea/jsLibraryMappings.xml 7 | 8 | ### Python template 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # Flask stuff: 15 | instance/ 16 | .webassets-cache 17 | 18 | # Log files 19 | install/PlatformInstall_* 20 | logs/*.log* 21 | 22 | # Sensitive information 23 | secret_csrf 24 | secret_key 25 | config.py 26 | parse.py 27 | service-account.json 28 | install/ci-vm/ci-windows/rclone.conf 29 | 30 | # Build svgs 31 | static/img/status/build-linux.svg 32 | static/img/status/build-windows.svg 33 | 34 | # Gunicorn 35 | gunicorn.pid 36 | 37 | # OS Generated Files 38 | .DS_Store 39 | 40 | # Coverage data 41 | coverage_report/ 42 | .coverage 43 | coverage.xml 44 | 45 | # virtualenv 46 | venv/ 47 | .vscode/ 48 | .idea/ 49 | .mypy_cache/ 50 | monkeytype.sqlite3 51 | .pytype/ 52 | .env 53 | 54 | # Test related data 55 | temp/ 56 | -------------------------------------------------------------------------------- /.pep8speaks.yml: -------------------------------------------------------------------------------- 1 | message: 2 | opened: 3 | header: "Hello @{name}, thank you for submitting this PR!" 4 | footer: "In need of guides? [Guidelines for PEP-8](https://pep8.org/) and [Hitchhiker's guide to code style](https://python-guide.readthedocs.io/en/latest/writing/style/)" 5 | updated: 6 | header: "Hello @{name}, Thank you for updating the PR!" 7 | footer: "" 8 | no_errors: "Great work! I discovered no PEP8 issues in this Pull Request. :1st_place_medal:" 9 | 10 | scanner: 11 | diff_only: True # Only show errors created by the new code 12 | 13 | pycodestyle: 14 | max-line-length: 120 # Default PEP8 is 79, but allow 120 because a lot of people use wider screens nowadays... 15 | ignore: E701 # We now use the syntax `x: int = 9` in typing 16 | exclude: TestDiff.py,migrations 17 | 18 | only_mention_files_with_errors: True 19 | descending_issues_order: False 20 | -------------------------------------------------------------------------------- /.pycodestylerc: -------------------------------------------------------------------------------- 1 | [pycodestyle] 2 | count = True 3 | max-line-length = 120 4 | exclude=test_diff.py,migrations,venv*,parse.py,config.py 5 | ignore = E701 6 | -------------------------------------------------------------------------------- /.pydocstylerc: -------------------------------------------------------------------------------- 1 | [pydocstyle] 2 | convention = numpy 3 | add-ignore=D100 4 | match_dir = ^(?!(venv|.venv|migrations|static|logs|install)).* 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Willem Van Iseghem 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /bootstrap_gunicorn.py: -------------------------------------------------------------------------------- 1 | """starts gunicorn server with appropriate arguments and options in a new process.""" 2 | 3 | import subprocess 4 | from os import path 5 | 6 | current_dir = path.dirname(path.abspath(__file__)) 7 | TIMEOUT = 120 # In seconds 8 | 9 | # Arguments to start gunicorn 10 | args = [ 11 | "gunicorn", "-w", "4", "--daemon", "--pid", "gunicorn.pid", "-b", "unix:sampleplatform.sock", "-m", "007", 12 | "-g", "www-data", "-u", "www-data", f"--chdir={current_dir}", "--log-level", "debug", "--timeout", f"{TIMEOUT}", 13 | "--access-logfile", f"{current_dir}/logs/access.log", "--capture-output", 14 | "--log-file", f"{current_dir}/logs/error.log", "run:app" 15 | ] 16 | 17 | subprocess.Popen(args) 18 | -------------------------------------------------------------------------------- /config_parser.py: -------------------------------------------------------------------------------- 1 | """parses configuration for the flask application, makes use of inbuilt method.""" 2 | 3 | from typing import Any, Dict 4 | 5 | from werkzeug.utils import import_string 6 | 7 | 8 | def parse_config(obj: str) -> Dict[Any, Any]: 9 | """ 10 | Parse given config either from a file or from an object. Method borrowed from Flask. 11 | 12 | :param obj: The config to parse. 13 | :type obj: any 14 | :return: A dictionary containing the parsed Flask config 15 | :rtype: dict 16 | """ 17 | config = {} 18 | if isinstance(obj, str): 19 | obj = import_string(obj) 20 | for key in dir(obj): 21 | if key.isupper(): 22 | config[key] = getattr(obj, key) 23 | return config 24 | -------------------------------------------------------------------------------- /config_sample.py: -------------------------------------------------------------------------------- 1 | """auto-generated module, using install.sh, to store configuration for the app.""" 2 | 3 | # For manual installation, fill in the fields below. If you are using 4 | # install.sh, the config.py should have been generated for you. 5 | APPLICATION_ROOT = None 6 | CSRF_ENABLED = True 7 | DATABASE_URI = 'mysql+pymysql://root:@localhost:3306/test?charset=utf8' 8 | GITHUB_TOKEN = '' 9 | GITHUB_OWNER = 'CCExtractor' 10 | GITHUB_REPOSITORY = 'ccextractor' 11 | SERVER_NAME = 'localhost' 12 | EMAIL_DOMAIN = '' 13 | EMAIL_API_KEY = '' 14 | HMAC_KEY = '' 15 | GITHUB_CI_KEY = '' 16 | GITHUB_CLIENT_ID = '' 17 | GITHUB_CLIENT_KEY = '' 18 | INSTALL_FOLDER = '/path/to/installation' 19 | SAMPLE_REPOSITORY = '/path/to/samples' 20 | SESSION_COOKIE_PATH = '/' 21 | FTP_PORT = 21 22 | MAX_CONTENT_LENGTH = 512 * 1024 * 1024 23 | MIN_PWD_LEN = 10 24 | MAX_PWD_LEN = 500 25 | 26 | 27 | # GCP SPECIFIC CONFIG 28 | SCOPES = ['https://www.googleapis.com/auth/cloud-platform'] 29 | SERVICE_ACCOUNT_FILE = 'service-account.json' 30 | ZONE = "us-west4-b" 31 | PROJECT_NAME = "ccextractor-sampleplatform" 32 | MACHINE_TYPE = f"zones/{ZONE}/machineTypes/n1-standard-1" 33 | WINDOWS_INSTANCE_PROJECT_NAME = "windows-cloud" 34 | WINDOWS_INSTANCE_FAMILY_NAME = "windows-2019" 35 | LINUX_INSTANCE_PROJECT_NAME = "ubuntu-os-cloud" 36 | LINUX_INSTANCE_FAMILY_NAME = "ubuntu-minimal-2404-lts-amd64" 37 | GCP_INSTANCE_MAX_RUNTIME = 120 # In minutes 38 | GCS_BUCKET_NAME = 'spdev' 39 | GCS_SIGNED_URL_EXPIRY_LIMIT = 720 # In minutes 40 | -------------------------------------------------------------------------------- /exceptions.py: -------------------------------------------------------------------------------- 1 | """Handle all the custom exceptions raised.""" 2 | import sys 3 | 4 | 5 | class QueuedSampleNotFoundException(Exception): 6 | """Custom exception handler for queued sample not found.""" 7 | 8 | def __init__(self, message: str) -> None: 9 | Exception.__init__(self) 10 | self.message = message 11 | 12 | 13 | class SampleNotFoundException(Exception): 14 | """Custom exception triggered when sample not found.""" 15 | 16 | def __init__(self, message: str) -> None: 17 | Exception.__init__(self) 18 | self.message = message 19 | 20 | 21 | class TestNotFoundException(Exception): 22 | """Custom exception handler for handling test not found.""" 23 | 24 | def __init__(self, message: str) -> None: 25 | Exception.__init__(self) 26 | self.message = message 27 | 28 | 29 | class SecretKeyInstallationException(Exception): 30 | """Custom exception handler for handling failed installation of secret keys.""" 31 | 32 | def __init__(self) -> None: 33 | Exception.__init__(self) 34 | sys.exit(1) 35 | 36 | 37 | class IncompleteConfigException(Exception): 38 | """Custom exception handler for handling missing configuration errors.""" 39 | 40 | pass 41 | 42 | 43 | class MissingConfigError(Exception): 44 | """Custom exception handler for handling missing config.py file.""" 45 | 46 | pass 47 | 48 | 49 | class FailedToSpawnDBSession(Exception): 50 | """Custom exception handler for handling failure of creating db session.""" 51 | 52 | pass 53 | 54 | 55 | class EnumParsingException(Exception): 56 | """Custom exception handler for handling failed parsing of Enum from string.""" 57 | 58 | pass 59 | 60 | 61 | class FailedToSendMail(Exception): 62 | """Custom exception handler for handling failure in sending mail.""" 63 | 64 | pass 65 | 66 | 67 | class MissingPathToCCExtractor(Exception): 68 | """Custom exception handler for handling the missing of CCExtractor with update sample method.""" 69 | 70 | def __init__(self) -> None: 71 | Exception.__init__(self) 72 | sys.exit(1) 73 | 74 | 75 | class CCExtractorEndedWithNonZero(Exception): 76 | """Custom exception handler for handling failure in producing new samples by CCExtractor.""" 77 | 78 | def __init__(self) -> None: 79 | Exception.__init__(self) 80 | sys.exit(1) 81 | -------------------------------------------------------------------------------- /install/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/install/__init__.py -------------------------------------------------------------------------------- /install/ci-vm/ci-linux/ci/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Bootstrap script that will get the runCI from the repository, provided the 4 | # VM is not in maintenance mode 5 | 6 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 7 | 8 | if [ ! -f "$DIR/variables" ]; then 9 | # No variable file defined 10 | sudo shutdown -h now 11 | fi 12 | 13 | # Source variables 14 | . "$DIR/variables" 15 | 16 | maintenance=$(curl ${maintenanceURL}) 17 | 18 | if [ "${maintenance}" == "True" ]; then 19 | # In debug mode 20 | exit; 21 | fi 22 | 23 | # Copy runCI to local folder 24 | cp ${runCIFile} "$DIR/runCI" 25 | chmod a+x runCI 26 | ./runCI 27 | -------------------------------------------------------------------------------- /install/ci-vm/ci-linux/ci/runCI: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script will run the test suite. It requires no parameters, but needs 4 | # some files to be present on the system. These are: 5 | # - file containing the URL to report to 6 | # - git repository with the code to compile & run 7 | 8 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 9 | 10 | if [ ! -f "$DIR/variables" ]; then 11 | # No variable file defined 12 | sudo shutdown -h now 13 | fi 14 | 15 | # Functions for re-use in various stages of the test progress 16 | 17 | # Post status to the server 18 | function postStatus { 19 | echo "Posting ${1} - ${2} to the server:" >> "${logFile}" 20 | curl -s -A "${userAgent}" --data "type=progress&status=$1&message=$2" -w "\n" "${reportURL}" >> "${logFile}" 21 | sleep 5 22 | } 23 | 24 | # Send the log file to the server so it can be used 25 | function sendLogFile { 26 | echo "Sending log to the server:" >> "${logFile}" 27 | curl -s -A "${userAgent}" --form "type=logupload" --form "file=@${logFile}" -w "\n" "${reportURL}" 28 | sleep 5 29 | } 30 | 31 | # Exit script and post abort status 32 | function haltAndCatchFire { 33 | sendLogFile 34 | postStatus "canceled" $1 >> "${logFile}" 35 | sudo shutdown -h now 36 | } 37 | 38 | # Fail when the exit status is not equal to 0 39 | function executeCommand { 40 | #echo "$@" 41 | "$@" >> "${logFile}" 42 | local status=$? 43 | if [ ${status} -ne 0 ]; then 44 | haltAndCatchFire "" # No message needed as we post before anyway 45 | fi 46 | } 47 | 48 | # Source variables 49 | . "$DIR/variables" 50 | 51 | # Add cargo to path 52 | PATH="/root/.cargo/bin:$PATH" 53 | 54 | reportURL=$(curl http://metadata/computeMetadata/v1/instance/attributes/reportURL -H "Metadata-Flavor: Google") 55 | userAgent="CCX/CI_BOT" 56 | logFile="${reportFolder}/log.html" 57 | 58 | postStatus "preparation" "Loaded variables, created log file and checking for CCExtractor build artifact" 59 | 60 | if [ -e "${dstDir}/ccextractor" ]; then 61 | cp $dstDir/* ./ 62 | chmod 700 ccextractor 63 | chmod +x ${tester} 64 | postStatus "testing" "Running tests" 65 | executeCommand cd ${suiteDstDir} 66 | executeCommand ${tester} --debug --entries "${testFile}" --executable "ccextractor" --tempfolder "${tempFolder}" --timeout 3000 --reportfolder "${reportFolder}" --resultfolder "${resultFolder}" --samplefolder "${sampleFolder}" --method Server --url "${reportURL}" 67 | sendLogFile 68 | postStatus "completed" "Ran all tests" 69 | 70 | sudo shutdown -h now 71 | else 72 | haltAndCatchFire "artifact" 73 | fi 74 | -------------------------------------------------------------------------------- /install/ci-vm/ci-linux/ci/variables: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script contains the variables which are used during CI on VM instances. 4 | # Copy/rename this file in case you want to change the default variables. 5 | 6 | # The URL to check if the VM is in maintenance 7 | maintenanceURL="http://foo.bar/maintenance/linux" 8 | # The runCI file URL 9 | runCIFile="/repository/TestData/ci-linux/runCI" 10 | # Directory where the test suite is located 11 | suiteDstDir="/repository" 12 | # Shell script to launch the test suite 13 | tester="/repository/ccextractortester" 14 | # Location of the samples 15 | sampleFolder="/repository/TestFiles" 16 | # Location of the result files 17 | resultFolder="/repository/TestResults" 18 | # Testfile location 19 | testFile="/repository/vm_data/ci-tests/TestAll.xml" 20 | # The folder that will be used to store the results in 21 | reportFolder="/repository/reports" 22 | # The folder that will be used to temporarily store the result files in 23 | tempFolder="/repository/TempFiles" 24 | # Directory where ccextractor executable is located 25 | dstDir="/repository/vm_data/unsafe-ccextractor" 26 | -------------------------------------------------------------------------------- /install/ci-vm/ci-linux/startup-script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | curl -L -O https://github.com/GoogleCloudPlatform/gcsfuse/releases/download/v0.39.2/gcsfuse_0.39.2_amd64.deb 4 | dpkg --install gcsfuse_0.39.2_amd64.deb 5 | rm gcsfuse_0.39.2_amd64.deb 6 | 7 | apt install gnupg ca-certificates 8 | gpg --homedir /tmp --no-default-keyring --keyring /usr/share/keyrings/mono-official-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF 9 | echo "deb [signed-by=/usr/share/keyrings/mono-official-archive-keyring.gpg] https://download.mono-project.com/repo/ubuntu stable-focal main" | sudo tee /etc/apt/sources.list.d/mono-official-stable.list 10 | sudo apt update 11 | apt install -y mono-complete libtesseract-dev libgpac-dev tesseract-ocr-eng 12 | 13 | mkdir repository 14 | cd repository 15 | 16 | # Use gcsfuse and import required files 17 | mkdir temp TestFiles TestResults vm_data reports 18 | 19 | gcs_bucket=$(curl http://metadata/computeMetadata/v1/instance/attributes/bucket -H "Metadata-Flavor: Google") 20 | 21 | vm_name=$(curl http://metadata.google.internal/computeMetadata/v1/instance/hostname -H "Metadata-Flavor: Google") 22 | vm_name=(${vm_name//./ }) 23 | 24 | echo "${gcs_bucket} /repository/temp gcsfuse rw,noatime,async,_netdev,noexec,user,implicit_dirs,allow_other,only_dir=TestData/ci-linux 0 0" | sudo tee -a /etc/fstab 25 | echo "${gcs_bucket} /repository/vm_data gcsfuse rw,noatime,async,_netdev,noexec,user,implicit_dirs,allow_other,only_dir=vm_data/${vm_name} 0 0" | sudo tee -a /etc/fstab 26 | echo "${gcs_bucket} /repository/TestFiles gcsfuse rw,noatime,async,_netdev,noexec,user,implicit_dirs,allow_other,only_dir=TestFiles 0 0" | sudo tee -a /etc/fstab 27 | echo "${gcs_bucket} /repository/TestResults gcsfuse rw,noatime,async,_netdev,noexec,user,implicit_dirs,allow_other,only_dir=TestResults 0 0" | sudo tee -a /etc/fstab 28 | 29 | mount temp 30 | mount vm_data 31 | mount TestFiles 32 | mount TestResults 33 | 34 | cp temp/* ./ 35 | 36 | chmod +x bootstrap 37 | ./bootstrap 38 | -------------------------------------------------------------------------------- /install/ci-vm/ci-windows/ci/runCI.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | echo Checking for the existence of variables.bat 4 | if NOT EXIST "variables.bat" ( 5 | rem No variable file defined 6 | shutdown -s -t 0 7 | exit 8 | ) 9 | 10 | echo Loading variables.bat 11 | rem Source variables 12 | call %~dp0\variables.bat 13 | 14 | for /F %%R in ('curl http://metadata/computeMetadata/v1/instance/attributes/reportURL -H "Metadata-Flavor: Google"') do SET reportURL=%%R 15 | SET userAgent="CCX/CI_BOT" 16 | SET logFile="%reportFolder%/log.html" 17 | 18 | call :postStatus "preparation" "Loaded variables, created log file and checking for CCExtractor build artifact" >> "%logFile%" 19 | 20 | echo Checking for CCExtractor build artifact 21 | if EXIST "%dstDir%\ccextractorwinfull.exe" ( 22 | echo Run tests 23 | copy "%dstDir%\*" . 24 | call :postStatus "testing" "Running tests" 25 | call :executeCommand cd %suiteDstDir% 26 | call :executeCommand "%tester%" --debug True --entries "%testFile%" --executable "ccextractorwinfull.exe" --tempfolder "%tempFolder%" --timeout 3000 --reportfolder "%reportFolder%" --resultfolder "%resultFolder%" --samplefolder "%sampleFolder%" --method Server --url "%reportURL%" 27 | 28 | curl -s -A "%userAgent%" --form "type=logupload" --form "file=@%logFile%" -w "\n" "%reportURL%" >> "%logFile%" 29 | timeout 10 30 | 31 | echo Done running tests 32 | call :postStatus "completed" "Ran all tests" 33 | 34 | shutdown -s -t 0 35 | exit 36 | ) else ( 37 | call :haltAndCatchFire "artifact" 38 | ) 39 | echo End 40 | EXIT %ERRORLEVEL% 41 | rem Functions to shorten the script 42 | 43 | rem Fail when the exit status is not equal to 0 44 | :executeCommand 45 | echo %* >> "%logFile%" 46 | %* >> "%logFile%" 47 | SET /A status=%ERRORLEVEL% 48 | IF %status% NEQ 0 ( 49 | echo Command exited with %status% >> "%logFile%" 50 | rem No message needed as we post before anyway 51 | call :haltAndCatchFire "" 52 | ) 53 | EXIT /B 0 54 | 55 | rem Post status to the server 56 | :postStatus 57 | echo "Posting status %~1 (message: %~2) to the server" 58 | curl -s -A "%userAgent%" --data "type=progress&status=%~1&message=%~2" -w "\n" "%reportURL%" >> "%logFile%" 59 | timeout 10 60 | EXIT /B 0 61 | 62 | rem Exit script and post abort status 63 | :haltAndCatchFire 64 | echo "Halt and catch fire (reason: %~1)" 65 | echo Post log 66 | curl -s -A "%userAgent%" --form "type=logupload" --form "file=@%logFile%" -w "\n" "%reportURL%" >> "%logFile%" 67 | rem Shut down, but only in 10 seconds, to give the time to finish the post status 68 | timeout 10 69 | call :postStatus "canceled" "%~1" 70 | shutdown -s -t 0 71 | EXIT 0 72 | -------------------------------------------------------------------------------- /install/ci-vm/ci-windows/ci/variables.bat: -------------------------------------------------------------------------------- 1 | :: batch file for variables 2 | 3 | :: This script contains the variables which are used during CI on VM instances. 4 | :: Copy/rename this file in case you want to change the default variables. 5 | 6 | @echo off 7 | :: Directory where ccextractor executable is located 8 | set dstDir=.\vm_data\unsafe-ccextractor 9 | :: Directory where the test suite is located 10 | set suiteDstDir=. 11 | :: Shell script to launch the test suite 12 | set tester=.\ccextractortester 13 | :: Location of the samples 14 | set sampleFolder=.\TestFiles 15 | :: Location of the result files 16 | set resultFolder=.\TestResults 17 | :: Testfile location 18 | set testFile=.\vm_data\ci-tests\TestAll.xml 19 | :: The folder that will be used to store the results in 20 | set reportFolder=.\reports 21 | -------------------------------------------------------------------------------- /install/ci-vm/ci-windows/rclone_sample.conf: -------------------------------------------------------------------------------- 1 | [GCS_BUCKET_NAME] 2 | type = google cloud storage 3 | project_number = GCP_PROJECT_NUMBER 4 | service_account_file = .\service-account.json 5 | object_acl = projectPrivate 6 | bucket_acl = projectPrivate 7 | location = GCS_BUCKET_LOCATION 8 | storage_class = GCS_BUCKET_LOCATION_TYPE 9 | bucket_policy_only = true 10 | -------------------------------------------------------------------------------- /install/ci-vm/ci-windows/startup-script.ps1: -------------------------------------------------------------------------------- 1 | Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) 2 | choco install winfsp -y 3 | 4 | cd C:\Windows\Temp 5 | 6 | curl.exe https://downloads.rclone.org/v1.59.0/rclone-v1.59.0-windows-amd64.zip --output rclone.zip 7 | Expand-Archive -Path rclone.zip -DestinationPath .\ 8 | New-Item -Path '.\repository' -ItemType Directory 9 | Copy-Item -Path .\rclone-v1.59.0-windows-amd64\rclone.exe -Destination .\repository\ 10 | Add-MpPreference -ExclusionProcess 'C:\Windows\Temp\repository\rclone.exe' 11 | 12 | cd repository 13 | New-Item -Path '.\reports' -ItemType Directory 14 | 15 | $gcs_bucket = curl.exe http://metadata/computeMetadata/v1/instance/attributes/bucket -H "Metadata-Flavor: Google" 16 | $env:mount_path = $gcs_bucket, $gcs_bucket -join ":" 17 | 18 | $env:vm_name = curl.exe http://metadata.google.internal/computeMetadata/v1/instance/hostname -H "Metadata-Flavor: Google" 19 | $env:vm_name = ($env:vm_name -split "\.")[0] 20 | 21 | curl.exe http://metadata/computeMetadata/v1/instance/attributes/rclone_conf -H "Metadata-Flavor: Google" > rclone.conf 22 | (Get-Content -path .\rclone.conf) | Set-Content -Encoding UTF8 -Path .\rclone.conf 23 | 24 | curl.exe http://metadata/computeMetadata/v1/instance/attributes/service_account -H "Metadata-Flavor: Google" > service-account.json 25 | (Get-Content -path .\service-account.json) | Set-Content -Encoding ASCII -Path .\service-account.json 26 | 27 | start powershell {.\rclone.exe mount $env:mount_path\TestFiles .\TestFiles --config=".\rclone.conf" --no-console --read-only} 28 | Start-Sleep -Seconds 5 29 | 30 | start powershell {.\rclone.exe mount $env:mount_path\TestData\ci-windows .\temp --config=".\rclone.conf" --no-console --read-only} 31 | Start-Sleep -Seconds 5 32 | 33 | start powershell {.\rclone.exe mount $env:mount_path\vm_data\$env:vm_name .\vm_data --config=".\rclone.conf" --no-console --read-only} 34 | Start-Sleep -Seconds 5 35 | 36 | Copy-Item -Path "temp\*" -Destination "." 37 | .\rclone.exe copy $env:mount_path\TestResults .\TestResults --config=".\rclone.conf" 38 | 39 | powershell -command "Start-Process runCI.bat -Verb runas" 40 | -------------------------------------------------------------------------------- /install/init_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | from os import path 5 | 6 | # Need to append server root path to ensure we can import the necessary files. 7 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 8 | 9 | if len(sys.argv) != 5: 10 | print(f'Invalid number of arguments. Expected 5 arguments, got {len(sys.argv)}') 11 | exit() 12 | 13 | 14 | def run(): 15 | from database import create_session 16 | from mod_auth.models import Role, User 17 | 18 | db = create_session(sys.argv[1]) 19 | # Check if there's at least one admin user 20 | admin = User.query.filter(User.role == Role.admin).first() 21 | if admin is not None: 22 | print(f"Admin already exists: {admin.name}") 23 | return 24 | 25 | user = User(sys.argv[2], Role.admin, sys.argv[3], User.generate_hash(sys.argv[4])) 26 | db.add(user) 27 | db.commit() 28 | print(f"Admin user created with name: {user.name}") 29 | 30 | 31 | run() 32 | -------------------------------------------------------------------------------- /install/nginx.conf: -------------------------------------------------------------------------------- 1 | # HTTP, but we don't want this; redirect all permanently to SSL 2 | server { 3 | listen 80 default_server; 4 | listen [::]:80 default_server; 5 | server_name NGINX_HOST; 6 | 7 | location / { 8 | return 301 https://$server_name$request_uri; 9 | } 10 | 11 | location /.well-known/acme-challenge { 12 | root /tmp; 13 | } 14 | } 15 | 16 | server { 17 | listen 443 ssl; 18 | listen [::]:443 ssl; 19 | server_name NGINX_HOST; 20 | 21 | # SSL stuff 22 | add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload"; 23 | 24 | ssl_certificate NGINX_CERT; 25 | ssl_certificate_key NGINX_KEY; 26 | 27 | ssl_session_tickets on; 28 | ssl_session_timeout 5m; 29 | ssl_session_cache shared:SSL:10m; 30 | 31 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; 32 | ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH'; 33 | ssl_prefer_server_ciphers on; 34 | ssl_ecdh_curve secp384r1; 35 | 36 | location ^~ /static/ { 37 | # Serve static files with Nginx 38 | include /etc/nginx/mime.types; 39 | root NGINX_DIR; 40 | } 41 | 42 | location / { 43 | try_files $uri @proxy_to_app; 44 | } 45 | 46 | location @proxy_to_app { 47 | include proxy_params; 48 | proxy_pass http://unix:NGINX_DIR/sampleplatform.sock; 49 | } 50 | } -------------------------------------------------------------------------------- /install/platform: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # /etc/init.d/platform 3 | # 4 | # Carry out specific functions when asked to by the system 5 | case "${1}" in 6 | start) 7 | echo "Starting Platform daemon..." 8 | cd BASE_DIR 9 | python bootstrap_gunicorn.py 10 | ;; 11 | stop) 12 | echo "Stopping Platform daemon..." 13 | pid=`cat "BASE_DIR/gunicorn.pid"` 14 | kill "${pid}" 15 | ;; 16 | reload) 17 | echo "Reloading Platform daemon..." 18 | pid=`cat "BASE_DIR/gunicorn.pid"` 19 | kill -HUP "${pid}" 20 | ;; 21 | *) 22 | echo "Usage: /etc/init.d/platform {start|stop|reload}" 23 | exit 1 24 | ;; 25 | esac 26 | 27 | exit 0 -------------------------------------------------------------------------------- /install/sample_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | from os import path 5 | 6 | from sqlalchemy.exc import IntegrityError 7 | 8 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 9 | 10 | 11 | def run(): 12 | from database import create_session 13 | from mod_auth.models import User 14 | from mod_customized.models import CustomizedTest 15 | from mod_home.models import CCExtractorVersion, GeneralData 16 | from mod_regression.models import (Category, InputType, OutputType, 17 | RegressionTest, RegressionTestOutput) 18 | from mod_sample.models import Sample 19 | from mod_test.models import Test 20 | from mod_upload.models import Upload 21 | 22 | db = create_session(sys.argv[1]) 23 | 24 | entries = [] 25 | categories = [ 26 | Category('Broken', 'Samples that are broken'), 27 | Category('DVB', 'Samples that contain DVB subtitles'), 28 | Category('DVD', 'Samples that contain DVD subtitles'), 29 | Category('MP4', 'Samples that are stored in the MP4 format'), 30 | Category('General', 'General regression samples') 31 | ] 32 | entries.extend(categories) 33 | 34 | samples = [ 35 | Sample('sample1', 'ts', 'sample1'), 36 | Sample('sample2', 'ts', 'sample2') 37 | ] 38 | entries.extend(samples) 39 | 40 | cc_version = CCExtractorVersion('0.84', '2016-12-16T00:00:00Z', '77da2dc873cc25dbf606a3b04172aa9fb1370f32') 41 | entries.append(cc_version) 42 | 43 | regression_tests = [ 44 | RegressionTest(1, '-autoprogram -out=ttxt -latin1', InputType.file, OutputType.file, 3, 10), 45 | RegressionTest(2, '-autoprogram -out=ttxt -latin1 -ucla', InputType.file, OutputType.file, 1, 10) 46 | ] 47 | entries.extend(regression_tests) 48 | 49 | gen_data = GeneralData('last_commit', '71dffd6eb30c1f4b5cf800307de845072ce33262') 50 | entries.append(gen_data) 51 | 52 | regression_test_output = [ 53 | RegressionTestOutput(1, "test1", "srt", "test1.srt"), 54 | RegressionTestOutput(2, "test2", "srt", "test2.srt") 55 | ] 56 | entries.extend(regression_test_output) 57 | 58 | for entry in entries: 59 | try: 60 | db.add(entry) 61 | db.commit() 62 | except IntegrityError: 63 | print("Entry already exists!", entry, flush=True) 64 | db.rollback() 65 | 66 | 67 | run() 68 | -------------------------------------------------------------------------------- /install/sample_files/sample1.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/install/sample_files/sample1.ts -------------------------------------------------------------------------------- /install/sample_files/sample2.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/install/sample_files/sample2.ts -------------------------------------------------------------------------------- /log_configuration.py: -------------------------------------------------------------------------------- 1 | """manages configuring logging for the app.""" 2 | 3 | import logging 4 | import logging.handlers 5 | import os 6 | from logging import Logger, StreamHandler 7 | from logging.handlers import RotatingFileHandler 8 | from typing import Union 9 | 10 | 11 | class LogConfiguration: 12 | """handle common logging options for the entire project.""" 13 | 14 | def __init__(self, folder: str, filename: str, debug: bool = False) -> None: 15 | # create console handler 16 | self._consoleLogger = logging.StreamHandler() 17 | self._consoleLogger.setFormatter(logging.Formatter('[%(levelname)s] %(message)s')) 18 | if debug: 19 | self._consoleLogger.setLevel(logging.DEBUG) 20 | else: 21 | self._consoleLogger.setLevel(logging.INFO) 22 | # create a file handler 23 | path = os.path.join(folder, 'logs', f'{filename}.log') 24 | self._fileLogger = logging.handlers.RotatingFileHandler(path, maxBytes=1024 * 1024, backupCount=20) 25 | self._fileLogger.setLevel(logging.DEBUG) 26 | # create a logging format 27 | formatter = logging.Formatter('[%(name)s][%(levelname)s][%(asctime)s] %(message)s') 28 | self._fileLogger.setFormatter(formatter) 29 | 30 | @property 31 | def file_logger(self) -> RotatingFileHandler: 32 | """ 33 | Get file logger. 34 | 35 | :return: file logger 36 | :rtype: logging.handlers.RotatingFileHandler 37 | """ 38 | return self._fileLogger 39 | 40 | @property 41 | def console_logger(self) -> StreamHandler: 42 | """ 43 | Get console logger. 44 | 45 | :return: console logger 46 | :rtype: logging.StreamHandler 47 | """ 48 | return self._consoleLogger 49 | 50 | def create_logger(self, name: str) -> Logger: 51 | """ 52 | Create new logger for the app. 53 | 54 | :param name: name for the logger 55 | :type name: str 56 | :return: logger 57 | :rtype: logging.Logger 58 | """ 59 | logger = logging.getLogger(name) 60 | logger.setLevel(logging.DEBUG) 61 | # add the handlers to the logger 62 | logger.addHandler(self.file_logger) 63 | logger.addHandler(self.console_logger) 64 | 65 | return logger 66 | -------------------------------------------------------------------------------- /logs/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mailer.py: -------------------------------------------------------------------------------- 1 | """handles the mailing operations across the app.""" 2 | 3 | import traceback 4 | from typing import Dict 5 | 6 | import requests 7 | from requests.models import Response 8 | 9 | from exceptions import FailedToSendMail 10 | 11 | 12 | class Mailer: 13 | """Send a mail through the Mailgun API.""" 14 | 15 | auth = None 16 | api_url = None 17 | sender = None 18 | 19 | def __init__(self, domain: str, api_key: str, sender_name: str) -> None: 20 | """ 21 | Initialize the Mailer class. 22 | 23 | :param domain: Domain name of the sender 24 | :type domain: str 25 | :param api_key: API key of the Mailgun API 26 | :type api_key: str 27 | :param sender_name: name of the person sending the email 28 | :type sender_name: str 29 | """ 30 | self.auth = ("api", api_key) 31 | self.api_url = f"https://api.mailgun.net/v3/{domain}" 32 | self.sender = f"{sender_name} " 33 | 34 | def send_simple_message(self, data: Dict) -> Response: 35 | """ 36 | Send a message. 37 | 38 | :param data: A dict consisting of the data for email 39 | :type data: dict 40 | :return: A Response object 41 | :rtype: requests.Response 42 | """ 43 | data['from'] = self.sender 44 | try: 45 | return requests.post(f"{self.api_url}/messages", auth=self.auth, data=data) 46 | except (requests.HTTPError, requests.ConnectionError): 47 | traceback.print_exc() 48 | raise FailedToSendMail 49 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python3 2 | """Root module to manage flask script commands.""" 3 | from flask_script import Command, Manager 4 | 5 | from exceptions import CCExtractorEndedWithNonZero, MissingPathToCCExtractor 6 | from mod_regression.update_regression import update_expected_results 7 | from run import app 8 | 9 | manager = Manager(app) 10 | 11 | 12 | @manager.add_command 13 | class UpdateResults(Command): 14 | """ 15 | Update results for the present samples with new ccextractor version. 16 | 17 | Pass path to CCExtractor binary as the first argument. Example, `python manage.py update /path/to/ccextractor` 18 | """ 19 | 20 | name = 'update' 21 | capture_all_args = True 22 | 23 | def run(self, remaining): 24 | """Driver function for update command.""" 25 | if len(remaining) == 0: 26 | print('path to ccextractor is missing') 27 | raise MissingPathToCCExtractor 28 | 29 | path_to_ccex = remaining[0] 30 | print(f'path to ccextractor: {path_to_ccex}') 31 | 32 | if not update_expected_results(path_to_ccex): 33 | print('update function errored') 34 | raise CCExtractorEndedWithNonZero 35 | 36 | print('update function finished') 37 | return 0 38 | 39 | 40 | if __name__ == '__main__': 41 | manager.run() 42 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | # add your model's MetaData object here 8 | # for 'autogenerate' support 9 | # from myapp import mymodel 10 | # target_metadata = mymodel.Base.metadata 11 | from flask import current_app 12 | from sqlalchemy import engine_from_config, pool 13 | 14 | # this is the Alembic Config object, which provides 15 | # access to the values within the .ini file in use. 16 | config = context.config 17 | 18 | if config.config_file_name is None: 19 | raise ValueError 20 | # Interpret the config file for Python logging. 21 | # This line sets up loggers basically. 22 | fileConfig(config.config_file_name) 23 | logger = logging.getLogger('alembic.env') 24 | 25 | config.set_main_option( 26 | 'sqlalchemy.url', current_app.config.get( 27 | 'SQLALCHEMY_DATABASE_URI').replace('%', '%%')) 28 | target_metadata = current_app.extensions['migrate'].db.metadata 29 | 30 | # other values from the config, defined by the needs of env.py, 31 | # can be acquired: 32 | # my_important_option = config.get_main_option("my_important_option") 33 | # ... etc. 34 | 35 | 36 | def run_migrations_offline(): 37 | """Run migrations in 'offline' mode. 38 | 39 | This configures the context with just a URL 40 | and not an Engine, though an Engine is acceptable 41 | here as well. By skipping the Engine creation 42 | we don't even need a DBAPI to be available. 43 | 44 | Calls to context.execute() here emit the given string to the 45 | script output. 46 | 47 | """ 48 | url = config.get_main_option("sqlalchemy.url") 49 | context.configure( 50 | url=url, target_metadata=target_metadata, literal_binds=True 51 | ) 52 | 53 | with context.begin_transaction(): 54 | context.run_migrations() 55 | 56 | 57 | def run_migrations_online(): 58 | """Run migrations in 'online' mode. 59 | 60 | In this scenario we need to create an Engine 61 | and associate a connection with the context. 62 | 63 | """ 64 | 65 | # this callback is used to prevent an auto-migration from being generated 66 | # when there are no changes to the schema 67 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 68 | def process_revision_directives(context, revision, directives): 69 | if getattr(config.cmd_opts, 'autogenerate', False): 70 | script = directives[0] 71 | if script.upgrade_ops.is_empty(): 72 | directives[:] = [] 73 | logger.info('No changes in schema detected.') 74 | 75 | connectable = engine_from_config( 76 | config.get_section(config.config_ini_section), 77 | prefix='sqlalchemy.', 78 | poolclass=pool.NullPool, 79 | ) 80 | 81 | with connectable.connect() as connection: 82 | context.configure( 83 | connection=connection, 84 | target_metadata=target_metadata, 85 | process_revision_directives=process_revision_directives, 86 | **current_app.extensions['migrate'].configure_args 87 | ) 88 | 89 | with context.begin_transaction(): 90 | context.run_migrations() 91 | 92 | 93 | if context.is_offline_mode(): 94 | run_migrations_offline() 95 | else: 96 | run_migrations_online() 97 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /migrations/versions/2e0d2e02a721_.py: -------------------------------------------------------------------------------- 1 | """ Upgrade: Rename 'kvm' table to 'gcp_instance', remove CCExtractor build step 2 | 3 | Revision ID: 2e0d2e02a721 4 | Revises: 6b1274f61edd 5 | Create Date: 2022-09-23 11:03:38.783561 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | from sqlalchemy.dialects import mysql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '2e0d2e02a721' 14 | down_revision = '6b1274f61edd' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.drop_column('kvm', 'timestamp_build_finished') 22 | op.alter_column('regression_test', 'expected_rc', 23 | existing_type=mysql.INTEGER(), 24 | nullable=True, 25 | existing_server_default=sa.text("'0'")) 26 | op.drop_constraint('regression_test_ibfk_1', 'regression_test', type_='foreignkey') 27 | op.create_foreign_key('regression_test_fk', 'regression_test', 'sample', ['sample_id'], ['id'], onupdate='CASCADE', ondelete='CASCADE') 28 | op.alter_column('test_result', 'runtime', 29 | existing_type=mysql.INTEGER(), 30 | nullable=True) 31 | op.alter_column('test_result', 'exit_code', 32 | existing_type=mysql.INTEGER(), 33 | nullable=True) 34 | op.alter_column('test_result', 'expected_rc', 35 | existing_type=mysql.INTEGER(), 36 | nullable=True, 37 | existing_server_default=sa.text("'0'")) 38 | op.drop_constraint('upload_ibfk_1', 'upload', type_='foreignkey') 39 | op.create_foreign_key('upload_fk', 'upload', 'user', ['user_id'], ['id'], onupdate='CASCADE', ondelete='RESTRICT') 40 | op.execute('RENAME TABLE kvm TO gcp_instance') 41 | op.execute("DELETE FROM test_progress WHERE status NOT IN ('completed', 'testing', 'preparation', 'canceled')") 42 | op.execute("ALTER TABLE test_progress MODIFY COLUMN status ENUM('completed', 'testing', 'preparation', 'canceled') NOT NULL") 43 | # ### end Alembic commands ### 44 | 45 | 46 | def downgrade(): 47 | # ### commands auto generated by Alembic - please adjust! ### 48 | op.execute("ALTER TABLE test_progress MODIFY COLUMN status ENUM('completed', 'testing', 'preparation', 'canceled', 'building') NOT NULL") 49 | op.execute('RENAME TABLE gcp_instance TO kvm') 50 | op.drop_constraint('upload_fk', 'upload', type_='foreignkey') 51 | op.create_foreign_key('upload_ibfk_1', 'upload', 'user', ['user_id'], ['id'], ondelete='RESTRICT') 52 | op.alter_column('test_result', 'expected_rc', 53 | existing_type=mysql.INTEGER(), 54 | nullable=False, 55 | existing_server_default=sa.text("'0'")) 56 | op.alter_column('test_result', 'exit_code', 57 | existing_type=mysql.INTEGER(), 58 | nullable=False) 59 | op.alter_column('test_result', 'runtime', 60 | existing_type=mysql.INTEGER(), 61 | nullable=False) 62 | op.drop_constraint('regression_test_fk', 'regression_test', type_='foreignkey') 63 | op.create_foreign_key('regression_test_ibfk_1', 'regression_test', 'sample', ['sample_id'], ['id'], onupdate='CASCADE') 64 | op.alter_column('regression_test', 'expected_rc', 65 | existing_type=mysql.INTEGER(), 66 | nullable=False, 67 | existing_server_default=sa.text("'0'")) 68 | op.add_column('kvm', sa.Column('timestamp_build_finished', mysql.DATETIME(), nullable=True)) 69 | # ### end Alembic commands ### 70 | -------------------------------------------------------------------------------- /migrations/versions/6b1274f61edd_.py: -------------------------------------------------------------------------------- 1 | """empty message 2 | 3 | Revision ID: 6b1274f61edd 4 | Revises: 6b335fbd58ab 5 | Create Date: 2019-07-10 22:56:20.082864 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | from sqlalchemy.dialects import mysql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '6b1274f61edd' 14 | down_revision = '6b335fbd58ab' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('kvm', sa.Column('timestamp_build_finished', sa.DateTime(), nullable=True)) 22 | op.add_column('kvm', sa.Column('timestamp_prep_finished', sa.DateTime(), nullable=True)) 23 | # ### end Alembic commands ### 24 | 25 | 26 | def downgrade(): 27 | # ### commands auto generated by Alembic - please adjust! ### 28 | op.drop_column('kvm', 'timestamp_prep_finished') 29 | op.drop_column('kvm', 'timestamp_build_finished') 30 | # ### end Alembic commands ### 31 | -------------------------------------------------------------------------------- /migrations/versions/a5183973c3e9_.py: -------------------------------------------------------------------------------- 1 | """Change SQLAlchemy Boolean bit(1) to bool 2 | 3 | Revision ID: a5183973c3e9 4 | Revises: 2e0d2e02a721 5 | Create Date: 2023-08-10 15:51:13.537000 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'a5183973c3e9' 13 | down_revision = '2e0d2e02a721' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | op.alter_column('regression_test', 'active', existing_type=sa.dialects.mysql.BIT(1), type_=sa.Boolean(), nullable=False) 20 | 21 | 22 | def downgrade(): 23 | op.alter_column('regression_test', 'active', existing_type=sa.Boolean(), type_=sa.dialects.mysql.BIT(1), nullable=False) 24 | -------------------------------------------------------------------------------- /migrations/versions/b3ed927671bd_.py: -------------------------------------------------------------------------------- 1 | """Add last_passed_on and description fields in regression_test 2 | 3 | Revision ID: b3ed927671bd 4 | Revises: a5183973c3e9 5 | Create Date: 2023-08-17 00:41:01.237549 6 | 7 | """ 8 | import sqlalchemy as sa 9 | from alembic import op 10 | from sqlalchemy.dialects import mysql 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'b3ed927671bd' 14 | down_revision = 'a5183973c3e9' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | with op.batch_alter_table('regression_test', schema=None) as batch_op: 22 | batch_op.add_column(sa.Column('last_passed_on', sa.Integer(), nullable=True)) 23 | batch_op.create_foreign_key('regression_test_ibfk_2', 'test', ['last_passed_on'], ['id'], onupdate='CASCADE', ondelete='SET NULL') 24 | batch_op.add_column(sa.Column('description', sa.String(length=1024), nullable=True)) 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | with op.batch_alter_table('regression_test', schema=None) as batch_op: 31 | batch_op.drop_constraint('regression_test_ibfk_2', type_='foreignkey') 32 | batch_op.drop_column('last_passed_on') 33 | batch_op.drop_column('description') 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /mod_auth/__init__.py: -------------------------------------------------------------------------------- 1 | """handle logic, models and forms related to authentication.""" 2 | -------------------------------------------------------------------------------- /mod_auth/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Maintain all the database models used in authentication. 3 | 4 | List of models corresponding to mysql tables: ['User' => 'user'] 5 | """ 6 | 7 | import string 8 | from typing import Any, Dict, Tuple, Type 9 | 10 | from passlib.apps import custom_app_context as pwd_context 11 | from sqlalchemy import Column, Integer, String, Text 12 | 13 | import database 14 | from database import Base, DeclEnum 15 | 16 | 17 | class Role(DeclEnum): 18 | """Roles available for users.""" 19 | 20 | admin = "admin", "Admin" 21 | user = "user", "User" 22 | contributor = "contributor", "Contributor" 23 | tester = "tester", "Tester" 24 | 25 | 26 | class User(Base): 27 | """Model for an user.""" 28 | 29 | __tablename__ = 'user' 30 | __table_args__ = {'mysql_engine': 'InnoDB'} 31 | id = Column(Integer, primary_key=True) 32 | name = Column(String(50), unique=True) 33 | email = Column(String(255), unique=True, nullable=True) 34 | github_token = Column(Text(), nullable=True) 35 | password = Column(String(255), unique=False, nullable=False) 36 | role = Column(Role.db_type()) 37 | 38 | def __init__(self, name, role=Role.user, email=None, password='', github_token=None) -> None: 39 | """ 40 | Parametrized constructor for the User model. 41 | 42 | :param name: The value of the 'name' field of User model 43 | :type name: str 44 | :param role: The value of the 'role' field of User model 45 | :type role: Role 46 | :param email: The value of the 'email' field of User model (None by 47 | default) 48 | :type email: str 49 | :param password: The value of the 'password' field of User model ( 50 | empty by default) 51 | :type password: str 52 | """ 53 | self.name = name 54 | self.email = email 55 | self.password = password 56 | self.role = role 57 | self.github_token = github_token 58 | 59 | def __repr__(self) -> str: 60 | """ 61 | Represent a User Model by its 'name' Field. 62 | 63 | :return str(name): Returns the string containing 'name' field 64 | of the User model 65 | :rtype str(name): str 66 | """ 67 | return f'' 68 | 69 | @staticmethod 70 | def generate_hash(password: str) -> str: 71 | """ 72 | Generate a Hash value for a password. 73 | 74 | :param password: The password to be hashed 75 | :type password: str 76 | :return : The hashed password 77 | :rtype : str 78 | """ 79 | # Go for increased strength no matter what 80 | return pwd_context.encrypt(password, category='admin') 81 | 82 | @staticmethod 83 | def create_random_password(length=16) -> str: 84 | """ 85 | Create a random password of default length 16. 86 | 87 | :param length: If parameter is passed, length will be the parameter. 88 | 16 by default 89 | :type length: int 90 | :return : Randomly generated password 91 | :rtype : str 92 | """ 93 | chars = string.ascii_letters + string.digits + '!@#$%^&*()' 94 | import os 95 | return ''.join(chars[ord(os.urandom(1)) % len(chars)] for i in range(length)) 96 | 97 | def is_password_valid(self, password) -> Any: 98 | """ 99 | Check the validity of the password. 100 | 101 | :param password: The password to be validated 102 | :type password: str 103 | :return : Validity of password 104 | :rtype : boolean 105 | """ 106 | return pwd_context.verify(password, self.password) 107 | 108 | def update_password(self, new_password) -> None: 109 | """ 110 | Update the password to a new one. 111 | 112 | :param new_password: The new password to be updated 113 | :type new_password: str 114 | """ 115 | self.password = self.generate_hash(new_password) 116 | 117 | @property 118 | def is_admin(self): 119 | """ 120 | Verify if an User is a admin. 121 | 122 | :return : Checks if User has an admin role 123 | :rtype: boolean 124 | """ 125 | return self.role == Role.admin 126 | 127 | def has_role(self, name) -> Any: 128 | """ 129 | Check whether the User has a particular role. 130 | 131 | :param name: Role of the user 132 | :type name: str 133 | :return : Checks whether a User has 'name' role 134 | :rtype: boolean 135 | """ 136 | return self.role.value == name or self.is_admin 137 | -------------------------------------------------------------------------------- /mod_ci/__init__.py: -------------------------------------------------------------------------------- 1 | """contains methods, forms and models for continuous integration.""" 2 | -------------------------------------------------------------------------------- /mod_ci/cron.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """handle cron logic for CI platform.""" 3 | 4 | import sys 5 | from os import path 6 | 7 | # Need to append server root path to ensure we can import the necessary files. 8 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 9 | 10 | 11 | def cron(testing=False): 12 | """Script to run from cron for Sampleplatform.""" 13 | from flask import current_app 14 | from github import Github 15 | 16 | from database import create_session 17 | from mod_ci.controllers import TestPlatform, gcp_instance, start_platforms 18 | from run import config, log 19 | 20 | log.info('Run the cron for kicking off CI platform(s).') 21 | gh = Github(config['GITHUB_TOKEN']) 22 | repository = gh.get_repo(f"{config['GITHUB_OWNER']}/{config['GITHUB_REPOSITORY']}") 23 | 24 | if testing is True: 25 | # Create a database session 26 | db = create_session(config['DATABASE_URI']) 27 | gcp_instance(current_app._get_current_object(), db, TestPlatform.linux, repository, None) 28 | else: 29 | start_platforms(repository) 30 | 31 | 32 | cron() 33 | -------------------------------------------------------------------------------- /mod_ci/forms.py: -------------------------------------------------------------------------------- 1 | """contains forms related to continuous integration operations.""" 2 | 3 | from flask_wtf import FlaskForm 4 | from wtforms import IntegerField, StringField, SubmitField 5 | from wtforms.validators import DataRequired 6 | 7 | 8 | class AddUsersToBlacklist(FlaskForm): 9 | """Form to add user to blacklist.""" 10 | 11 | user_id = IntegerField('User ID', [DataRequired(message='GitHub User ID not filled in')]) 12 | comment = StringField('Comment') 13 | add = SubmitField('Add User') 14 | 15 | 16 | class DeleteUserForm(FlaskForm): 17 | """Form to remove user from blacklist.""" 18 | 19 | submit = SubmitField('Remove') 20 | -------------------------------------------------------------------------------- /mod_customized/__init__.py: -------------------------------------------------------------------------------- 1 | """contains methods, models and forms related to running custom tests by users.""" 2 | -------------------------------------------------------------------------------- /mod_customized/forms.py: -------------------------------------------------------------------------------- 1 | """contains forms related to creating customized tests.""" 2 | 3 | from flask_wtf import FlaskForm 4 | from wtforms import (RadioField, SelectMultipleField, StringField, SubmitField, 5 | widgets) 6 | from wtforms.validators import DataRequired, url 7 | 8 | from mod_regression.models import RegressionTest 9 | from mod_test.models import TestPlatform 10 | 11 | 12 | class MultiCheckboxField(SelectMultipleField): 13 | """Provide multi-input checkbox.""" 14 | 15 | widget = widgets.ListWidget(prefix_label=False) 16 | option_widget = widgets.CheckboxInput() 17 | 18 | 19 | class TestForkForm(FlaskForm): 20 | """Form to test user's fork.""" 21 | 22 | commit_hash = StringField('Commit Hash', [DataRequired(message='Commit hash is not filled in')]) 23 | commit_select = RadioField('Choose Commit', choices=[('', '')], default='') 24 | platform = MultiCheckboxField('Platform', validators=[DataRequired()], choices=[( 25 | platform, platform) for platform in TestPlatform.values()]) 26 | regression_test = MultiCheckboxField('Regression Test', validators=[DataRequired( 27 | message='Please add one or more Regression Tests')], coerce=int) 28 | add = SubmitField('Run Test') 29 | -------------------------------------------------------------------------------- /mod_customized/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Maintain models for database operations regarding custom tests run by users. 3 | 4 | List of models corresponding to mysql tables: ['TestFork' => 'test_fork', 5 | 'CustomizedTest' => 'customized_test'] 6 | """ 7 | 8 | from typing import Any, Dict, Type 9 | 10 | from sqlalchemy import Column, ForeignKey, Integer 11 | from sqlalchemy.orm import relationship 12 | 13 | import mod_auth.models 14 | import mod_regression.models 15 | import mod_test.models 16 | from database import Base 17 | from mod_auth.models import User 18 | from mod_regression.models import RegressionTest 19 | from mod_test.models import Test 20 | 21 | 22 | class TestFork(Base): 23 | """Model to manage custom [set of] test by user.""" 24 | 25 | __tablename__ = 'test_fork' 26 | __table_args__ = {'mysql_engine': 'InnoDB'} 27 | id = Column(Integer, primary_key=True) 28 | user_id = Column(Integer, ForeignKey(User.id, onupdate="CASCADE", ondelete="RESTRICT")) 29 | user = relationship('User', uselist=False) 30 | test_id = Column(Integer, ForeignKey(Test.id, onupdate="CASCADE", ondelete="RESTRICT")) 31 | test = relationship('Test', uselist=False) 32 | 33 | def __init__(self, user_id, test_id) -> None: 34 | """ 35 | Parametrized constructor for the CCExtractorVersion model. 36 | 37 | :param user_id: The value of the 'user_id' field of 38 | TestFork model 39 | :type version: int 40 | :param test_id: The value of the 'test_id' field of 41 | TestFork model 42 | :type test_id: int 43 | """ 44 | self.user_id = user_id 45 | self.test_id = test_id 46 | 47 | 48 | class CustomizedTest(Base): 49 | """Store custom tests pertaining to a test.""" 50 | 51 | __tablename__ = 'customized_test' 52 | __table_args__ = {'mysql_engine': 'InnoDB'} 53 | id = Column(Integer, primary_key=True) 54 | test_id = Column(Integer, ForeignKey(Test.id, onupdate="CASCADE", ondelete="RESTRICT")) 55 | test = relationship('Test', back_populates='customized_tests') 56 | regression_id = Column(Integer, ForeignKey(RegressionTest.id, onupdate='CASCADE', ondelete='CASCADE')) 57 | regression_test = relationship('RegressionTest', uselist=False) 58 | 59 | def __init__(self, test_id, regression_id) -> None: 60 | """ 61 | Parametrized constructor for the CustomizedTest model. 62 | 63 | :param test_id: The value of the 'test_id' field of 64 | CustomizedTest model 65 | :type test_id: int 66 | :param regression_id: The value of the 'regression_id' field of 67 | CustomizedTest model 68 | :type regression_id: int 69 | """ 70 | self.test_id = test_id 71 | self.regression_id = regression_id 72 | -------------------------------------------------------------------------------- /mod_home/__init__.py: -------------------------------------------------------------------------------- 1 | """Manages logic and models related to homepage and CCExtractor data.""" 2 | -------------------------------------------------------------------------------- /mod_home/controllers.py: -------------------------------------------------------------------------------- 1 | """maintains all functionalities running on homepage.""" 2 | from flask import Blueprint, g 3 | 4 | from decorators import template_renderer 5 | from mod_auth.models import Role 6 | from mod_home.models import CCExtractorVersion, GeneralData 7 | 8 | mod_home = Blueprint('home', __name__) 9 | 10 | 11 | @mod_home.before_app_request 12 | def before_app_request() -> None: 13 | """Curate menu entries before app request.""" 14 | g.menu_entries['home'] = { 15 | 'title': 'Home', 16 | 'icon': 'home', 17 | 'route': 'home.index' 18 | } 19 | 20 | 21 | @mod_home.route('/', methods=['GET', 'POST']) 22 | @template_renderer() 23 | def index(): 24 | """Render index home page.""" 25 | last_commit = GeneralData.query.filter(GeneralData.key == 'last_commit').first().value 26 | last_release = CCExtractorVersion.query.order_by(CCExtractorVersion.released.desc()).first() 27 | test_access = False 28 | if g.user is not None and g.user.role in [Role.tester, Role.contributor, Role.admin]: 29 | test_access = True 30 | return { 31 | 'ccx_last_release': last_release, 32 | 'ccx_latest_commit': last_commit, 33 | 'test_access': test_access 34 | } 35 | 36 | 37 | @mod_home.route('/about') 38 | @template_renderer() 39 | def about(): 40 | """Render about page.""" 41 | return {} 42 | -------------------------------------------------------------------------------- /mod_home/models.py: -------------------------------------------------------------------------------- 1 | """ 2 | Maintains database models regarding general information. 3 | 4 | Includes version, released date, commit about main repository (CCExtractor). 5 | 6 | List of models corresponding to mysql tables: 7 | [ 8 | 'CCExtractorVersion' => 'ccextractor_version', 9 | 'GeneralData' => 'general_data' 10 | ] 11 | """ 12 | from datetime import datetime 13 | from typing import Any, Dict, Type 14 | 15 | from sqlalchemy import Column, Date, Integer, String, Text 16 | 17 | import database 18 | from database import Base, DeclEnum 19 | 20 | 21 | class CCExtractorVersion(Base): 22 | """Model to manage CCExtractor version and release data.""" 23 | 24 | __tablename__ = 'ccextractor_version' 25 | __table_args__ = {'mysql_engine': 'InnoDB'} 26 | id = Column(Integer, primary_key=True) 27 | version = Column(String(10), unique=True) 28 | released = Column(Date(), unique=True) 29 | commit = Column(String(64), unique=True) 30 | 31 | def __init__(self, version, released, commit) -> None: 32 | """ 33 | Parametrized constructor for the CCExtractorVersion model. 34 | 35 | :param version: The value of the 'version' field of 36 | CCExtractorVersion model 37 | :type version: str 38 | :param released: The value of the 'released' field of 39 | CCExtractorVersion model 40 | :type released: datetime 41 | :param commit: The value of the 'timestamp' field of 42 | CCExtractorVersion model 43 | :type commit: str 44 | """ 45 | self.version = version 46 | self.released = datetime.strptime(released, '%Y-%m-%dT%H:%M:%SZ').date() 47 | self.commit = commit 48 | 49 | def __repr__(self) -> str: 50 | """ 51 | Represent a CCExtractorVersion Model by its 'version' Field. 52 | 53 | :return str(version): Returns the string containing 54 | 'version' field of the CCExtractorVersion model 55 | :rtype str(version): str 56 | """ 57 | return f"" 58 | 59 | 60 | class GeneralData(Base): 61 | """Model to manage general data.""" 62 | 63 | __tablename__ = 'general_data' 64 | __table_args__ = {'mysql_engine': 'InnoDB'} 65 | id = Column(Integer, primary_key=True) 66 | key = Column(String(64), unique=True) 67 | value = Column(Text(), nullable=False) 68 | 69 | def __init__(self, key, value) -> None: 70 | """ 71 | Parametrized constructor for the GeneralData model. 72 | 73 | :param key: The value of the 'key' field of 74 | GeneralData model 75 | :type key: str 76 | :param value: The value of the 'value' field of 77 | GeneralData model 78 | :type value: str 79 | """ 80 | self.key = key 81 | self.value = value 82 | 83 | def __repr__(self) -> str: 84 | """ 85 | Represent a GeneralData Model by its 'key' and 'value' Field. 86 | 87 | :return str(key,version): Returns the string containing 88 | 'key' and 'version' field of the GeneralData model 89 | :rtype str(key,version): str 90 | """ 91 | return f"" 92 | -------------------------------------------------------------------------------- /mod_regression/__init__.py: -------------------------------------------------------------------------------- 1 | """Maintains logic, models and forms related to regression tests.""" 2 | -------------------------------------------------------------------------------- /mod_regression/forms.py: -------------------------------------------------------------------------------- 1 | """Maintain forms related to CRUD operations on regression tests.""" 2 | 3 | from flask_wtf import FlaskForm 4 | from wtforms import (HiddenField, IntegerField, SelectField, StringField, 5 | SubmitField, TextAreaField) 6 | from wtforms.validators import DataRequired, InputRequired, Length 7 | 8 | from mod_regression.models import InputType, OutputType 9 | 10 | 11 | class AddCategoryForm(FlaskForm): 12 | """Flask form to Add Category.""" 13 | 14 | category_name = StringField("Category Name", [DataRequired(message="Category name can't be empty")]) 15 | category_description = StringField("Description") 16 | submit = SubmitField("Add Category") 17 | 18 | 19 | class CommonTestForm(FlaskForm): 20 | """Common Flask form to manage a Regression Test.""" 21 | 22 | sample_id = SelectField("Sample", coerce=int) 23 | command = StringField("Command") 24 | description = TextAreaField("Description", validators=[Length(max=1024)]) 25 | input_type = SelectField( 26 | "Input Type", 27 | [DataRequired(message="Input Type is not selected")], 28 | coerce=str, 29 | choices=[(i.value, i.description) for i in InputType] 30 | ) 31 | output_type = SelectField( 32 | "Output Type", 33 | [DataRequired(message="Output Type is not selected")], 34 | coerce=str, 35 | choices=[(o.value, o.description) for o in OutputType] 36 | ) 37 | category_id = SelectField("Category", coerce=int) 38 | expected_rc = IntegerField("Expected Runtime Code", [InputRequired(message="Expected Runtime Code can't be empty")]) 39 | 40 | 41 | class AddTestForm(CommonTestForm): 42 | """Flask form to add a Regression Test.""" 43 | 44 | submit = SubmitField("Add Regression Test") 45 | 46 | 47 | class EditTestForm(CommonTestForm): 48 | """Flask form to edit a Regression Test.""" 49 | 50 | submit = SubmitField("Edit Regression Test") 51 | 52 | 53 | class ConfirmationForm(FlaskForm): 54 | """Flask Form Used for Asking Confirmations.""" 55 | 56 | confirm = HiddenField('confirm', default='yes') 57 | submit = SubmitField('Confirm') 58 | 59 | 60 | class AddCorrectOutputForm(FlaskForm): 61 | """Flask form to Add correct output.""" 62 | 63 | output_file = SelectField( 64 | "Choose an original file to which the variant file should be attached to", 65 | [DataRequired(message="Output cannot be empty")], 66 | coerce=int 67 | ) 68 | test_id = SelectField( 69 | "Choose a Result file from previous Test runs", 70 | [DataRequired(message="Output cannot be empty")], 71 | coerce=str, 72 | ) 73 | submit = SubmitField("Add Output") 74 | 75 | 76 | class RemoveCorrectOutputForm(FlaskForm): 77 | """Flask form to Remove correct output.""" 78 | 79 | output_file = SelectField( 80 | "Choose an output file (variant)", 81 | [DataRequired(message="Output cannot be empty")], 82 | coerce=int 83 | ) 84 | submit = SubmitField("Remove Output") 85 | -------------------------------------------------------------------------------- /mod_sample/__init__.py: -------------------------------------------------------------------------------- 1 | """Maintain logic, models, forms and parser related to samples and it's information.""" 2 | -------------------------------------------------------------------------------- /mod_sample/forms.py: -------------------------------------------------------------------------------- 1 | """Maintains forms related to sample CRUD operations.""" 2 | 3 | from flask_wtf import FlaskForm 4 | from wtforms import StringField, SubmitField, TextAreaField 5 | from wtforms.validators import DataRequired, ValidationError 6 | 7 | from mod_upload.forms import CommonSampleForm 8 | 9 | from .models import Tag 10 | 11 | 12 | class EditSampleForm(CommonSampleForm): 13 | """Form to edit sample.""" 14 | 15 | submit = SubmitField('Update sample') 16 | 17 | 18 | class AddTagForm(FlaskForm): 19 | """Form to add tags.""" 20 | 21 | name = StringField('Name', validators=[DataRequired(message="Tag name is required.")]) 22 | description = TextAreaField('Description') 23 | submit = SubmitField('Add Tag') 24 | 25 | @staticmethod 26 | def validate_name(form, field) -> None: 27 | """ 28 | Validate tag name (case-insensitive). 29 | 30 | :param form: form data 31 | :type form: AddTagForm 32 | :param field: field to validate 33 | :type field: form field 34 | :raises ValidationError: when the same tag already exists 35 | """ 36 | existing_tag = Tag.query.filter(Tag.name.ilike(field.data)).first() 37 | if existing_tag: 38 | raise ValidationError("Tag with the same name already exists.") 39 | 40 | 41 | class DeleteSampleForm(FlaskForm): 42 | """Form to delete sample.""" 43 | 44 | submit = SubmitField('Delete sample') 45 | 46 | 47 | class DeleteAdditionalSampleForm(FlaskForm): 48 | """Form to delete sample's additional file.""" 49 | 50 | submit = SubmitField('Delete extra file') 51 | -------------------------------------------------------------------------------- /mod_test/__init__.py: -------------------------------------------------------------------------------- 1 | """Maintains logic and models for managing tests and forming results.""" 2 | -------------------------------------------------------------------------------- /mod_test/nicediff/__init__.py: -------------------------------------------------------------------------------- 1 | """Maintains logic for diffs.""" 2 | -------------------------------------------------------------------------------- /mod_upload/__init__.py: -------------------------------------------------------------------------------- 1 | """Maintain logic, models and forms related to upload and it's progress.""" 2 | -------------------------------------------------------------------------------- /mod_upload/forms.py: -------------------------------------------------------------------------------- 1 | """Maintain forms to perform CRUD operations on uploads and related database.""" 2 | 3 | import mimetypes 4 | import os 5 | 6 | import magic 7 | from flask_wtf import FlaskForm 8 | from wtforms import (FileField, SelectField, SelectMultipleField, SubmitField, 9 | TextAreaField) 10 | from wtforms.validators import DataRequired, ValidationError 11 | 12 | from mod_home.models import CCExtractorVersion 13 | from mod_sample.models import ForbiddenExtension, ForbiddenMimeType 14 | from mod_upload.models import Platform 15 | 16 | 17 | class UploadForm(FlaskForm): 18 | """Form to make a new sample upload.""" 19 | 20 | accept = '.ts, .txt, .srt, .png, video/*' 21 | 22 | file = FileField('File to upload', [DataRequired(message='No file was provided.')], render_kw={'accept': accept}) 23 | submit = SubmitField('Upload file') 24 | 25 | @staticmethod 26 | def validate_file(form, field) -> None: 27 | """ 28 | Validate sample being uploaded. 29 | 30 | :param form: form data 31 | :type form: UploadForm 32 | :param field: field to validate 33 | :type field: form field 34 | :raises ValidationError: when extension is not allowed 35 | :raises ValidationError: when mimetype is not allowed 36 | :raises ValidationError: when extension not provided and not supported 37 | """ 38 | # File cannot end with a forbidden extension 39 | filename, file_extension = os.path.splitext(field.data.filename) 40 | if len(file_extension) > 0: 41 | forbidden_ext = ForbiddenExtension.query.filter(ForbiddenExtension.extension == file_extension[1:]).first() 42 | if forbidden_ext is not None: 43 | raise ValidationError('Extension not allowed') 44 | mimetype = magic.from_buffer(field.data.read(1024), mime=True) 45 | # File Pointer returns to beginning 46 | field.data.seek(0, 0) 47 | # Check for permitted mimetype 48 | forbidden_mime = ForbiddenMimeType.query.filter(ForbiddenMimeType.mimetype == mimetype).first() 49 | if forbidden_mime is not None: 50 | raise ValidationError('File MimeType not allowed') 51 | extension = mimetypes.guess_extension(mimetype) 52 | if extension is not None: 53 | forbidden_real = ForbiddenExtension.query.filter(ForbiddenExtension.extension == extension[1:]).first() 54 | if forbidden_real is not None: 55 | raise ValidationError('Extension not allowed') 56 | 57 | 58 | class DeleteQueuedSampleForm(FlaskForm): 59 | """Form to delete a queued sample.""" 60 | 61 | submit = SubmitField('Delete queued file') 62 | 63 | 64 | class CommonSampleForm(FlaskForm): 65 | """Form to submit common sample data.""" 66 | 67 | notes = TextAreaField('Notes', [DataRequired(message='Notes are not filled in')]) 68 | parameters = TextAreaField('Parameters', [DataRequired(message='Parameters are not filled in')]) 69 | platform = SelectField( 70 | 'Platform', 71 | [DataRequired(message='Platform is not selected')], 72 | coerce=str, 73 | choices=[(p.value, p.description) for p in Platform] 74 | ) 75 | tags = SelectMultipleField('Tags', coerce=int) 76 | version = SelectField('Version', [DataRequired(message='Version is not selected')], coerce=int) 77 | 78 | @staticmethod 79 | def validate_version(form, field) -> None: 80 | """ 81 | Validate CCExtractor version. 82 | 83 | :param form: form data 84 | :type form: CommonSampleForms 85 | :param field: field to validate 86 | :type field: form field 87 | :raises ValidationError: when invalid version selected 88 | """ 89 | version = CCExtractorVersion.query.filter(CCExtractorVersion.id == field.data).first() 90 | if version is None: 91 | raise ValidationError('Invalid version selected') 92 | 93 | 94 | class FinishQueuedSampleForm(CommonSampleForm): 95 | """Form to finalize sample queue.""" 96 | 97 | report = SelectField('Do you want to report an issue on GitHub?', choices=[('n', 'No'), ('y', 'Yes')]) 98 | IssueTitle = TextAreaField('Issue Title', [DataRequired(message='Title is not filled in')]) 99 | IssueBody = TextAreaField('Issue Content', [DataRequired(message='Content is not filled in')]) 100 | submit = SubmitField('Finalize sample') 101 | -------------------------------------------------------------------------------- /mod_upload/progress_ftp_upload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """Maintain logic to show upload progress.""" 3 | 4 | import sys 5 | from os import path 6 | 7 | # Need to append server root path to ensure we can import the necessary files. 8 | sys.path.append(path.dirname(path.dirname(path.abspath(__file__)))) 9 | 10 | 11 | def process(database, file_to_process): 12 | """ 13 | Call ftp upload method. 14 | 15 | :param database: database 16 | :type database: database cursor 17 | :param file_to_process: path of file to upload 18 | :type file_to_process: str 19 | """ 20 | from mod_upload.controllers import upload_ftp 21 | from run import log 22 | log.debug("Calling the FTP upload method from the controller!") 23 | upload_ftp(database, file_to_process) 24 | 25 | 26 | if __name__ == '__main__': 27 | from database import create_session 28 | from run import config, log 29 | 30 | db = create_session(config['DATABASE_URI']) 31 | file_path = str(sys.argv[1]) 32 | process(db, file_path) 33 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.8 3 | ignore_missing_imports = True 4 | warn_unused_ignores = True 5 | exclude = venv* 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | sqlalchemy==1.4.41 2 | flask==1.1.2 3 | passlib==1.7.4 4 | pymysql==1.1.1 5 | python-magic==0.4.27 6 | flask-wtf==1.1.1 7 | requests==2.32.2 8 | pyIsEmail==2.0.1 9 | GitPython==3.1.41 10 | xmltodict==0.13.0 11 | lxml==4.9.3 12 | pytz==2023.3.post1 13 | tzlocal==4.1 14 | markdown2==2.4.10 15 | flask-migrate==4.0.7 16 | flask-script==2.0.6 17 | email_validator 18 | gitdb==4.0.10 19 | Werkzeug==1.0.1 20 | WTForms==2.3.3 21 | MarkupSafe<=2.1.3 22 | jinja2==3.0.2 23 | itsdangerous==2.0.1 24 | google-api-python-client==2.111.0 25 | google-cloud-storage==2.10.0 26 | cffi==1.15.1 27 | PyGithub==1.58.2 28 | -------------------------------------------------------------------------------- /static/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | #ffffff 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /static/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /static/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /static/img/favicon/android-chrome-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/android-chrome-144x144.png -------------------------------------------------------------------------------- /static/img/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/img/favicon/android-chrome-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/android-chrome-36x36.png -------------------------------------------------------------------------------- /static/img/favicon/android-chrome-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/android-chrome-48x48.png -------------------------------------------------------------------------------- /static/img/favicon/android-chrome-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/android-chrome-72x72.png -------------------------------------------------------------------------------- /static/img/favicon/android-chrome-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/android-chrome-96x96.png -------------------------------------------------------------------------------- /static/img/favicon/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /static/img/favicon/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /static/img/favicon/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /static/img/favicon/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /static/img/favicon/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /static/img/favicon/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /static/img/favicon/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /static/img/favicon/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /static/img/favicon/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /static/img/favicon/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /static/img/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /static/img/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /static/img/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /static/img/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /static/img/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/favicon.ico -------------------------------------------------------------------------------- /static/img/favicon/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/mstile-144x144.png -------------------------------------------------------------------------------- /static/img/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /static/img/favicon/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/mstile-310x150.png -------------------------------------------------------------------------------- /static/img/favicon/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/mstile-310x310.png -------------------------------------------------------------------------------- /static/img/favicon/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/favicon/mstile-70x70.png -------------------------------------------------------------------------------- /static/img/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 20 | 28 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /static/img/filezilla.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/img/filezilla.png -------------------------------------------------------------------------------- /static/img/status/.keep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /static/js/same-blocks-highlight.js: -------------------------------------------------------------------------------- 1 | window.onload = () => { 2 | let elems = document.getElementsByClassName('diff-same-region'); 3 | 4 | for (let i = 0; i < elems.length; i++) { 5 | let e = elems[i]; 6 | e.onmouseover = () => { 7 | let pairId = ""; 8 | if (e.id.indexOf("test_result") != -1) { 9 | pairId = e.id.replace("test_result", "correct"); 10 | } else { 11 | pairId = e.id.replace("correct", "test_result"); 12 | } 13 | let pair = document.getElementById(pairId); 14 | e.style.borderBottom = '1px dotted #3BA3D0'; 15 | pair.style.borderBottom = '1px dotted #3BA3D0'; 16 | } 17 | 18 | e.onmouseout = () => { 19 | let pairId = ""; 20 | if (e.id.indexOf("test_result") != -1) { 21 | pairId = e.id.replace("test_result", "correct"); 22 | } else { 23 | pairId = e.id.replace("correct", "test_result"); 24 | } 25 | let pair = document.getElementById(pairId); 26 | e.style.borderBottom = ''; 27 | pair.style.borderBottom = ''; 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /static/js/sorttable.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/js/sorttable.js -------------------------------------------------------------------------------- /static/js/theme.js: -------------------------------------------------------------------------------- 1 | $(document).ready(() => { 2 | $('.theme-toggle i.theme').on('click', toggleTheme); 3 | // Toggle Icon Not Shown Until Page Completely Loads 4 | 5 | // Fetch default theme from localStorage and apply it. 6 | const theme = localStorage.getItem("data-theme"); 7 | if (theme === 'dark') toggleTheme(); 8 | else $('.theme-toggle i.to-dark').removeClass('hidden'); 9 | }); 10 | 11 | 12 | function toggleTheme() { 13 | document.documentElement.toggleAttribute("dark"); 14 | const theme = document.querySelector("#theme-link"); 15 | 16 | const toDark = $('.theme-toggle i.to-dark'); 17 | const toLight = $('.theme-toggle i.to-light'); 18 | // Not Allowing User to Toggle Theme Again Before This Gets Completed. 19 | toDark.addClass('hidden') 20 | toLight.addClass('hidden') 21 | 22 | const lightThemeCSS = "/static/css/foundation-light.min.css"; 23 | const darkThemeCSS = "/static/css/foundation-dark.min.css"; 24 | 25 | if (theme.getAttribute("href") == lightThemeCSS) { 26 | theme.href = darkThemeCSS; 27 | localStorage.setItem("data-theme", "dark"); 28 | toLight.removeClass('hidden') 29 | } 30 | else { 31 | theme.href = lightThemeCSS; 32 | localStorage.setItem("data-theme", "light"); 33 | toDark.removeClass('hidden') 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CCExtractor sample platform", 3 | "icons": [ 4 | { 5 | "src": "img\/favicon\/android-chrome-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": 0.75 9 | }, 10 | { 11 | "src": "img\/favicon\/android-chrome-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": 1 15 | }, 16 | { 17 | "src": "img\/favicon\/android-chrome-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": 1.5 21 | }, 22 | { 23 | "src": "img\/favicon\/android-chrome-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": 2 27 | }, 28 | { 29 | "src": "img\/favicon\/android-chrome-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": 3 33 | }, 34 | { 35 | "src": "img\/favicon\/android-chrome-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": 4 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /static/svg/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/sample-platform/6186a314e45f786c19e26b7403b5d775a1dbb3ab/static/svg/.keep -------------------------------------------------------------------------------- /templates/400.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% set hideMenu = True %} 4 | {% block title %}400 - Bad Request{% endblock %} 5 | {% block body %} 6 | {{ super() }} 7 |
8 |
9 |
10 |

400 - Bad Request

11 |

Invalid request made to the server.

12 |
13 |
14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /templates/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% set hideMenu = True %} 4 | {% block title %}403 - Forbidden{% endblock %} 5 | {% block body %} 6 | {{ super() }} 7 |
8 |
9 |
10 |

403 - Forbidden

11 |

You don't have the permission to access the requested resource. It is either read-protected or not readable by the server.

12 |

Are you sure you're signed in with the correct account and that the URL is correct?

13 |

You tried to access this route: {{ endpoint }}, with a user role of {{ user_role }}

14 |
15 |
16 | {% endblock %} -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% set hideMenu = True %} 4 | {% block title %}404 - Not found{% endblock %} 5 | {% block body %} 6 | {{ super() }} 7 |
8 |
9 |
10 |

404 - Not found

11 |

The page you were looking for does not exist.

12 |
13 |
14 | {% endblock %} -------------------------------------------------------------------------------- /templates/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% set hideMenu = True %} 4 | {% block title %}500 - Internal server error{% endblock %} 5 | {% block body %} 6 | {{ super() }} 7 |
8 |
9 |
10 |

500 - Internal server error

11 |

This isn't supposed to happen :(

12 |

If this error persists, please get in touch.

13 |
14 |
15 | {% endblock %} -------------------------------------------------------------------------------- /templates/auth/complete_reset.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Password reset step 2 {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
Reset your password
10 |

Please enter your new password below:

11 |
12 |
13 |
14 | {{ form.csrf_token }} 15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 |
18 |
19 |
    20 | {% for category, message in messages %} 21 |
  • {{ message }}
  • 22 | {% endfor %} 23 |
24 |
25 |
26 | {% endif %} 27 | {% endwith %} 28 | {% if form.errors %} 29 |
30 |
31 | {% for field, error in form.errors.items() %} 32 | {% for e in error %} 33 | {{ e }}
34 | {% endfor %} 35 | {% endfor %} 36 |
37 |
38 | {% endif %} 39 |
40 |
41 | {{ macros.render_field(form.password) }} 42 |
43 |
44 | {{ macros.render_field(form.password_repeat) }} 45 |
46 |
47 | {{ macros.render_field(form.submit) }} 48 |
49 |
50 |
51 | {% endblock %} -------------------------------------------------------------------------------- /templates/auth/complete_signup.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Complete registration {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
Complete registration
10 |

Thank you for verifying the email address*! At this point, we need just a bit more information before we can complete your account.

11 |
12 |
13 |
14 | {{ form.csrf_token }} 15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 |
18 |
19 |
    20 | {% for category, message in messages %} 21 |
  • {{ message }}
  • 22 | {% endfor %} 23 |
24 |
25 |
26 | {% endif %} 27 | {% endwith %} 28 | {% if form.errors %} 29 |
30 |
31 | {% for field, error in form.errors.items() %} 32 | {% for e in error %} 33 | {{ e }}
34 | {% endfor %} 35 | {% endfor %} 36 |
37 |
38 | {% endif %} 39 |
40 |
41 | {{ macros.render_field(form.name) }} 42 |
43 |
44 | {{ macros.render_field(form.password) }} 45 |
46 |
47 | {{ macros.render_field(form.password_repeat) }} 48 |
49 |
50 | {{ macros.render_field(form.submit) }} 51 |
52 |
53 |

* Your email address isn't stored yet, so you can just leave this page if you changed your mind.

54 |
55 |
56 |
57 | {% endblock %} -------------------------------------------------------------------------------- /templates/auth/deactivate.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Deactivate user account {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
Account deactivation for {{ view_user.name }}
10 |

This action cannot be undone. Your details will be made anonymous, and you will not longer be able to log in. Are you sure you want to do this?

11 |
12 |
13 | {% if form.errors %} 14 |
15 |
16 | {% for field, error in form.errors.items() %} 17 | {% for e in error %} 18 | {{ e }}
19 | {% endfor %} 20 | {% endfor %} 21 |
22 |
23 | {% endif %} 24 |
25 |
26 |
27 | {{ form.csrf_token }} 28 |

29 | 30 | 31 |

32 |
33 |
34 |
35 | {% endblock %} -------------------------------------------------------------------------------- /templates/auth/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Please log in first {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
Login required
10 |

To get access to all the functionality of this application, you need to login below.

11 |
12 |
13 |
14 | {{ form.csrf_token }} 15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 |
18 |
19 |
    20 | {% for category, message in messages %} 21 |
  • {{ message }}
  • 22 | {% endfor %} 23 |
24 |
25 |
26 | {% endif %} 27 | {% endwith %} 28 | {% if form.errors %} 29 |
30 |
31 | {% for field, error in form.errors.items() %} 32 | {% for e in error %} 33 | {{ e }}
34 | {% endfor %} 35 | {% endfor %} 36 |
37 |
38 | {% endif %} 39 |
40 |
41 | {{ macros.render_field(form.email) }} 42 |
43 |
44 | {{ macros.render_field(form.password) }} 45 |
46 |
47 | {{ macros.render_field(form.submit) }} 48 |
49 |
50 |

Forgot your password? Reset your password.

51 |

No account? Sign up here.

52 |
53 |
54 |
55 | {% endblock %} -------------------------------------------------------------------------------- /templates/auth/manage.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Manage my account {{ super() }}{% endblock %} 4 | {% block menu %}{% include 'menu.html' %}{% endblock %} 5 | {% block body %} 6 | {{ super() }} 7 |
8 |
9 |
10 |

Manage my account

11 |

On this page you can change your password and email address.

12 |
13 |
14 | {{ form.csrf_token }} 15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 |
18 |
19 |
    20 | {% for category, message in messages %} 21 |
  • {{ message }}
  • 22 | {% endfor %} 23 |
24 |
25 |
26 | {% endif %} 27 | {% endwith %} 28 | {% if form.errors %} 29 |
30 |
31 | {% for field, error in form.errors.items() %} 32 | {% for e in error %} 33 | {{ e }}
34 | {% endfor %} 35 | {% endfor %} 36 |
37 |
38 | {% endif %} 39 |
40 |
41 | GitHub Linked: 42 | {% if url%} 43 | No   44 | Link GitHub 45 | {% else %} 46 | Yes 47 | {% endif %} 48 |
49 |
50 | Role: {{ user.role.value }} 51 |
52 |
53 |
54 | {{ macros.render_field(form.current_password) }} 55 |
56 |
57 |
58 |
59 | {{ macros.render_field(form.new_password, helpText='Your password must have at least 10 characters.') }} 60 |
61 |
62 | {{ macros.render_field(form.new_password_repeat) }} 63 |
64 |
65 |
66 |
67 | {{ macros.render_field(form.name) }} 68 |
69 |
70 |
71 |
72 | {{ macros.render_field(form.email) }} 73 |
74 |
75 |
76 | {{ macros.render_field(form.submit) }} 77 |
78 |
79 |

If you want to deactivate your account, you can go to this page: deactivate account.

80 |
81 |
82 |
83 |
84 | {% endblock %} -------------------------------------------------------------------------------- /templates/auth/reset.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Recover password {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
Recover password
10 |

In order to recover your password we need the email address linked to the account so we can send an email with instructions.

11 |
12 |
13 |
14 | {{ form.csrf_token }} 15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 |
18 |
19 |
    20 | {% for category, message in messages %} 21 |
  • {{ message }}
  • 22 | {% endfor %} 23 |
24 |
25 |
26 | {% endif %} 27 | {% endwith %} 28 | {% if form.errors %} 29 |
30 | {% for field, error in form.errors.items() %} 31 | {% for e in error %} 32 | {{ e }}
33 | {% endfor %} 34 | {% endfor %} 35 |
36 | {% endif %} 37 |
38 |
39 | {{ macros.render_field(form.email) }} 40 |
41 |
42 | {{ macros.render_field(form.submit) }} 43 |
44 |
45 |
46 | {% endblock %} -------------------------------------------------------------------------------- /templates/auth/reset_user.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Reset user password {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
Reset user password
10 |

An email to {{ view_user.name }} was sent with reset instructions.

11 |
12 |
13 | {% with messages = get_flashed_messages(with_categories=true) %} 14 | {% if messages %} 15 |
16 |
17 |
    18 | {% for category, message in messages %} 19 |
  • {{ message }}
  • 20 | {% endfor %} 21 |
22 |
23 |
24 | {% endif %} 25 | {% endwith %} 26 | {% endblock %} -------------------------------------------------------------------------------- /templates/auth/role.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Change user role {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
Change user role for {{ view_user.name }}
10 |

Select a new role for this user:

11 |
12 |
13 | {% if form.errors %} 14 |
15 |
16 | {% for field, error in form.errors.items() %} 17 | {% for e in error %} 18 | {{ e }}
19 | {% endfor %} 20 | {% endfor %} 21 |
22 |
23 | {% endif %} 24 |
25 |
26 |
27 | {{ form.csrf_token }} 28 |

{{ macros.render_field(form.role) }}

29 |

30 | 31 | 32 |

33 |
34 |
35 |
36 | {% endblock %} -------------------------------------------------------------------------------- /templates/auth/signup.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Sign up {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
Register
10 |

Please enter your email address so we can send an verification email

11 |
12 |
13 |
14 | {{ form.csrf_token }} 15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 |
18 |
19 |
    20 | {% for category, message in messages %} 21 |
  • {{ message }}
  • 22 | {% endfor %} 23 |
24 |
25 |
26 | {% endif %} 27 | {% endwith %} 28 | {% if form.errors %} 29 |
30 | {% for field, error in form.errors.items() %} 31 | {% for e in error %} 32 | {{ e }}
33 | {% endfor %} 34 | {% endfor %} 35 |
36 | {% endif %} 37 |
38 |
39 | {{ macros.render_field(form.email) }} 40 |
41 |
42 | {{ macros.render_field(form.submit) }} 43 |
44 |
45 |

Forgot your password? Reset your password.

46 |
47 |
48 |
49 | {% endblock %} -------------------------------------------------------------------------------- /templates/auth/user.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}User info for {{ view_user.name }} {{ super() }}{% endblock %} 4 | 5 | {% block body %} 6 | {{ super() }} 7 |
8 |
9 |

User details for {{ view_user.name }}

10 |

Note: only you and the site admin can see these details.

11 |

Name: {{ view_user.name }}

12 |

Email: {{ view_user.email }}

13 |

GitHub linked? {{ "Yes" if view_user.github_token is not none else "No" }}

14 |

Role: {{ view_user.role.description }}

15 |
16 |
17 |

Submitted samples

18 | {% set no_samples="This user didn't submit any samples yet." %} 19 | {% set use_sample_original=true %} 20 | {% include "sample/list_samples.html" %} 21 |
22 | {% endblock %} -------------------------------------------------------------------------------- /templates/auth/users.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Manage users {{ super() }}{% endblock %} 4 | 5 | {% block body %} 6 | {{ super() }} 7 |
8 |
9 |

Manage users

10 |

Below you can find an overview of all registered users on the platform.

11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {% for u in users %} 25 | 26 | 27 | 28 | 29 | 30 | 36 | 37 | {% endfor %} 38 | 39 |
#NameRoleEmailActions
{{ u.id }}{{ u.name }}{{ u.role.description }}{{ u.email }}
40 |
41 | {% endblock %} -------------------------------------------------------------------------------- /templates/ci/blocked_users.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Manage Blocked Users {{ super() }}{% endblock %} 4 | 5 | {% block body %} 6 | {{ super() }} 7 |
8 |
9 |

Manage Blocked Users

10 |

Below you can find an overview of all blocked users on the platform and add or remove them.

11 |
12 | {% with messages = get_flashed_messages(with_categories=true) %} 13 | {% if messages %} 14 |
15 |
16 |
    17 | {% for category, message in messages %} 18 |
  • {{ message }}
  • 19 | {% endfor %} 20 |
21 |
22 |
23 | {% endif %} 24 | {% endwith %} 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {% for u in blocked_users %} 37 | 38 | 39 | 40 | 41 | 42 | 43 | {% endfor %} 44 | 45 |
GitHub User IDUsernameCommentActions
{{ u.user_id }}{{ usernames[u.user_id] }}{{ u.comment }}
46 |
47 |
48 |

Add Users

49 |
50 |
51 | {{ addUserForm.csrf_token }} 52 | {% if addUserForm.errors %} 53 |
54 |
55 | {% for field, error in addUserForm.errors.items() %} 56 | {% for e in error %} 57 | {{ e }}
58 | {% endfor %} 59 | {% endfor %} 60 |
61 |
62 | {% endif %} 63 |
64 |
65 | {{ macros.render_field(addUserForm.user_id) }} 66 |
67 |
68 | {{ macros.render_field(addUserForm.comment) }} 69 |
70 |
71 | {{ macros.render_field(addUserForm.add) }} 72 |
73 |
74 |
75 | {% endblock %} 76 | -------------------------------------------------------------------------------- /templates/ci/blocked_users_remove.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Delete User from blacklist {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
10 |

Delete User from blacklist

11 |

Are you sure you want to delete the user with id {{ blocked_user_id }}?

12 |

This will delete the user from the blacklist!

13 |
14 | {{ form.csrf_token }} 15 |
16 |
17 | {{ macros.render_field(form.submit) }} 18 |
19 |
20 | 21 |
22 |
23 |
24 |
25 |
26 |
27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /templates/ci/maintenance.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Maintenance Mode {{ super() }}{% endblock %} 4 | {% block menu %}{% include 'menu.html' %}{% endblock %} 5 | {% block body %} 6 | {{ super() }} 7 |
8 |
9 |

Maintenance Mode

10 | {% for platform_status in platforms %} 11 |

{{ platform_status.platform.description }}

12 |
13 | 14 | 15 |
16 | {% endfor %} 17 |
18 | {% endblock %} 19 | {% block scripts %} 20 | {{ super() }} 21 | 43 | {% endblock %} -------------------------------------------------------------------------------- /templates/ci/pr_comment.txt: -------------------------------------------------------------------------------- 1 | CCExtractor CI platform finished running the test files on {{platform}}. Below is a summary of the test results{% if comment_info.last_test_master %}, when compared to test for commit {{ comment_info.last_test_master.commit[:7] }}...{% endif %}: 2 | 3 | 4 | 5 | 6 | 7 | {% for test in comment_info.category_stats %} 8 | 9 | 10 | {% if test.success==None %} 11 | 12 | {% elif test.success == test.total %} 13 | 14 | {% else %} 15 | 16 | {% endif %} 17 | 18 | {% endfor %} 19 |
Report Name Tests Passed
{{test.category}} 0/{{test.total}} {{test.success}}/{{test.total}} {{test.success}}/{{test.total}}
20 | {% if comment_info.extra_failed_tests | length %} 21 | It seems that not all tests were passed completely. This is an indication that the output of some files is not as expected (but might be according to you). 22 | 23 | Your PR breaks these cases: 24 |
    25 | {% for test in comment_info.extra_failed_tests %} 26 |
  • ccextractor {{ test.command }} {{ test.sample.sha[:10] }}...
  • 27 | {% endfor %} 28 |
29 | {% else %} 30 | All tests passing on the master branch were passed completely. 31 | {% endif %} 32 | {% if comment_info.common_failed_tests | length %} 33 | NOTE: The following tests have been failing on the master branch as well as the PR: 34 | 39 | {% endif %} 40 | {% if comment_info.fixed_tests | length %} 41 | Congratulations: Merging this PR would fix the following tests: 42 | 47 | {% endif %} 48 |
49 | Check the result page for more info. 50 | -------------------------------------------------------------------------------- /templates/email/email_changed.txt: -------------------------------------------------------------------------------- 1 | Dear {{ name }}, 2 | 3 | the email address used for your account on the CCExtractor CI platform has been changed to the address below: 4 | 5 | {{ email }} 6 | 7 | If you did not request this change, please get in touch as soon as possible. -------------------------------------------------------------------------------- /templates/email/new_issue.txt: -------------------------------------------------------------------------------- 1 | {{ title }} - {{ author }}
2 | Link to Issue: {{ url }}
3 | {{ author }}

4 | {{body}} -------------------------------------------------------------------------------- /templates/email/password_changed.txt: -------------------------------------------------------------------------------- 1 | Dear {{ name }}, 2 | 3 | your password has been changed on the CCExtractor CI platform. 4 | 5 | If you did not change the password yourself, please get in touch through the website. -------------------------------------------------------------------------------- /templates/email/password_reset.txt: -------------------------------------------------------------------------------- 1 | Dear {{ name }}, 2 | 3 | your password has been reset for the CCExtractor CI platform, following a request to change it. 4 | 5 | If you did not request this password reset, please get in touch through the website. -------------------------------------------------------------------------------- /templates/email/recovery_link.txt: -------------------------------------------------------------------------------- 1 | Dear {{ name }}, 2 | 3 | there has been a request to reset your password for your account on the CCExtractor CI platform. 4 | 5 | To change your password you need to click on the link below, or copy it into the browser: 6 | 7 | {{ url|safe }} 8 | 9 | Attention! This link is only valid for two hours after the request has been made, and can be used only ONCE to reset the password! 10 | 11 | If you did not request this password reset and you suspect an intrusion attempt, please get in touch through the website. -------------------------------------------------------------------------------- /templates/email/registration_email.txt: -------------------------------------------------------------------------------- 1 | Hello, 2 | 3 | somebody used this email address to initiate the account registration on the CCExtractor CI platform. If you want to proceed with this registration, please follow the link below. 4 | 5 | If you didn't request this, you can ignore this email (without the link the registration process can't be completed). The link below is valid for 24 hours. 6 | 7 | {{ url|safe }} -------------------------------------------------------------------------------- /templates/email/registration_existing.txt: -------------------------------------------------------------------------------- 1 | Hello {{ name }}, 2 | 3 | someone requested to register on the CCExtractor CI Platform with the email address you used to register before. 4 | 5 | If this request comes from you and you forgot your password, you can reset it here: {{ url|safe }}. 6 | 7 | If you didn't perform the registration procedure, you can ignore this email. -------------------------------------------------------------------------------- /templates/email/registration_ok.txt: -------------------------------------------------------------------------------- 1 | Dear {{ name }}, 2 | 3 | an account has been created on the CCExtractor Sample Submission platform for you. You can log in using the email address you are receiving this on in combination with the password you set during the registration. 4 | 5 | If you forgot your password you can always reset it through the website. -------------------------------------------------------------------------------- /templates/home/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}About {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |

{{ applicationName }}

10 |

11 | {{ applicationName }} was initially developed during Google Summer of Code 2015, and reworked during Google Summer of Code 2016 for CCExtractor. 12 | It combines a lot of technologies and methods in order to provide an all-in-one platform for submitting samples, viewing and running regression tests. 13 |

14 |

This instance of {{ applicationName }} is running version {{ applicationVersion }}. More information about the version can be found on the GitHub repository.

15 |

{{ applicationName }} is released under the ISC (permissive) license and can be found on GitHub: {{ applicationName }} on GitHub.

16 |
17 |
18 |
19 |

About this platform

20 |

{{ applicationName }} makes use of the next frameworks & open-source utilities:

21 | 34 |
35 |
36 | 37 |
38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /templates/home/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Home {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |

{{ applicationName }}

10 |

Welcome to the sample submission platform. On this platform you can do the next things:

11 |
    12 |
  • Check on the current status of the regression tests
  • 13 |
  • Upload samples that are broken
  • 14 |
  • Get information on previously submitted samples
  • 15 |
  • Run the regression tests
  • 16 | {% if test_access %} 17 |
  • Run your own tests if you have linked your GitHub
  • 18 | {% else %} 19 |
  • "Want to run your own tests? Send the admins a request" if you don't have the permissions yet
  • 20 | {% endif %} 21 |
22 |

Development information

23 |

Last official release: {{ ccx_last_release.version }} (release date: {{ ccx_last_release.released }}). Regression tests for this version are available here: regression tests

24 |

Latest GitHub commit: {{ ccx_latest_commit }}. View the regression tests.

25 |
26 |
27 |
Helpful links
28 | 33 |
34 |
35 | {% endblock %} -------------------------------------------------------------------------------- /templates/macros.html: -------------------------------------------------------------------------------- 1 | {% macro render_field(field, placeholder=None, helpText=none, replaceLabelWithPlaceholder=False) %} 2 | {% set css_class = kwargs.pop('class', '') %} 3 | {% if replaceLabelWithPlaceholder and placeholder is none %} 4 | {% set placeholder = field.label.text %} 5 | {% elif placeholder is none %} 6 | {% set placeholder = '' %} 7 | {% endif %} 8 | {% if not replaceLabelWithPlaceholder and field.type != 'SubmitField' and field.type != 'HiddenField' %} 9 | {{ field.label }} 10 | {% endif %} 11 | {% if field.type == 'SubmitField' %} 12 | {% set css_class = css_class + 'button' %} 13 | {% endif %} 14 | {% if helpText is not none %} 15 | {% set dummy = kwargs.setdefault('aria-describedby', field.id + '_help_text') %} 16 | {% endif %} 17 | {{ field(class=css_class, placeholder=placeholder, **kwargs) }} 18 | 19 | {% if helpText is not none %} 20 |

{{ helpText }}

21 | {% endif %} 22 | {% endmacro %} 23 | 24 | {% macro render_media_info(info) %} 25 |
    26 | {% for entry in info recursive %} 27 | {% if entry.value is iterable and not entry.value is string %} 28 |
  • 29 | {{ entry.name }}:
    30 |
      31 | {% if entry.value is mapping %} 32 | {% for key, value in entry.value.items() recursive %} 33 | {% if value is mapping %} 34 |
    • 35 | {{ key }}:
      36 |
        37 | {{ loop(value) }} 38 |
      39 |
    • 40 | {% elif value != '' %} 41 |
    • {{ key }}: {{ value }}
    • 42 | {% endif %} 43 | {% endfor %} 44 | {% elif entry.value|length == 0 %} 45 |
    • No entries available
    • 46 | {% else %} 47 | {{ loop(entry.value) }} 48 | {% endif %} 49 |
    50 |
  • 51 | {% elif value != "" %} 52 |
  • {{ entry.name }}: {{ entry.value }}
  • 53 | {% endif %} 54 | {% endfor %} 55 |
56 | {% endmacro %} -------------------------------------------------------------------------------- /templates/menu.html: -------------------------------------------------------------------------------- 1 | {% macro render_entry(data, active_route, level=0) %} 2 | {%- set href = "#" -%} 3 | {%- if 'entries' not in data -%} 4 | {%- if 'route_args' in data -%} 5 | {%- set href = url_for(data.route, **(data.route_args)) -%} 6 | {%- else -%} 7 | {%- set href = url_for(data.route) -%} 8 | {%- endif -%} 9 | {%- endif %} 10 | 11 | 0 %} class="subitem"{% endif %}> 12 | {{ data.title }} 13 | 14 | {%- if 'entries' in data -%} 15 | {%- set level = level + 1 -%} 16 | 21 | {%- endif %} 22 | 23 | {% endmacro %} 24 | 25 |
26 | 33 |
34 | -------------------------------------------------------------------------------- /templates/regression/by_sample.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Regression tests for sample {{ sample.id }} {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |

Regression tests for sample {{ sample.id }}

10 | {% if tests|length > 0 %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% for test in tests %} 21 | 22 | 23 | 24 | 31 | 32 | {% endfor %} 33 | 34 |
IDCommandActions
{{ test.id }}{{ test.command }} 25 |   26 | {% if user is not none and user.has_role('contributor') %} 27 |   28 |   29 | {% endif %} 30 |
35 | {% else %} 36 |

There are no regression tests available

37 | {% endif %} 38 |
39 |
40 | {% endblock %} -------------------------------------------------------------------------------- /templates/regression/category_add.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Category Add {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
Category Add
10 |

Please add new category

11 |
12 |
13 |
14 | {{ form.csrf_token }} 15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 |
18 |
19 |
    20 | {% for category, message in messages %} 21 |
  • {{ message }}
  • 22 | {% endfor %} 23 |
24 |
25 |
26 | {% endif %} 27 | {% endwith %} 28 |
29 |
30 | {{ macros.render_field(form.category_name) }} 31 |
32 |
33 | {{ macros.render_field(form.category_description) }} 34 |
35 |
36 | {{ macros.render_field(form.submit) }} 37 |
38 |
39 |
40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /templates/regression/category_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Category Delete {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
Category Delete
10 |

Are you sure you want to delete this? This change is irreversible!

11 |
12 |
13 |
14 | {{ form.csrf_token }} 15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 |
18 |
19 |
    20 | {% for category, message in messages %} 21 |
  • {{ message }}
  • 22 | {% endfor %} 23 |
24 |
25 |
26 | {% endif %} 27 | {% endwith %} 28 |
29 | 32 |
33 | {{ macros.render_field(form.submit) }} 34 |
35 |
36 |
37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /templates/regression/category_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Category Edit {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
Category Edit
10 |

Please edit the category

11 |
12 |
13 |
14 | {{ form.csrf_token }} 15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 |
18 |
19 |
    20 | {% for category, message in messages %} 21 |
  • {{ message }}
  • 22 | {% endfor %} 23 |
24 |
25 |
26 | {% endif %} 27 | {% endwith %} 28 |
29 |
30 | {{ macros.render_field(form.category_name) }} 31 |
32 |
33 | {{ macros.render_field(form.category_description) }} 34 |
35 |
36 | {{ macros.render_field(form.submit) }} 37 |
38 |
39 |
40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /templates/regression/output_add.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Regression Test Output {{ regression_id }} Add {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
Regression Test Output Add
10 |

Please add new correct output file for RegressionTest {{ regression_id }}

11 |
12 |
13 |
14 | {{ form.csrf_token }} 15 | {% if form.errors %} 16 |
17 |
18 | {% for field, error in form.errors.items() %} 19 | {{ error }}
20 | {% endfor %} 21 |
22 |
23 | {% endif %} 24 |
25 |
26 | {{ macros.render_field(form.output_file) }} 27 |
28 |
29 | {{ macros.render_field(form.test_id) }} 30 |
31 |
32 | {{ macros.render_field(form.submit) }} 33 |
34 |
35 |
36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /templates/regression/output_remove.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Regression Test Output {{ regression_id }} Add {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
Regression Test Output Remove
10 |

Please remove a correct output file for RegressionTest {{ regression_id }}

11 |
12 |
13 |
14 | {{ form.csrf_token }} 15 | {% if form.errors %} 16 |
17 |
18 | {% for field, error in form.errors.items() %} 19 | {{ error }}
20 | {% endfor %} 21 |
22 |
23 | {% endif %} 24 |
25 |
26 | {{ macros.render_field(form.output_file) }} 27 |
28 |
29 | {{ macros.render_field(form.submit) }} 30 |
31 |
32 |
33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /templates/regression/test_add.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Regression Test Add {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
Regression Test Add
10 |

Please add new regression test

11 |
12 |
13 |
14 | {{ form.csrf_token }} 15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 |
18 |
19 |
    20 | {% for category, message in messages %} 21 |
  • {{ message }}
  • 22 | {% endfor %} 23 |
24 |
25 |
26 | {% endif %} 27 | {% endwith %} 28 | {% if form.errors %} 29 |
30 |
31 | {% for field, error in form.errors.items() %} 32 | {{ error }}
33 | {% endfor %} 34 |
35 |
36 | {% endif %} 37 |
38 |
39 | {{ macros.render_field(form.sample_id) }} 40 |
41 |
42 | {{ macros.render_field(form.description) }} 43 |
44 |
45 | {{ macros.render_field(form.command) }} 46 |
47 |
48 | {{ macros.render_field(form.input_type) }} 49 |
50 |
51 | {{ macros.render_field(form.output_type) }} 52 |
53 |
54 | {{ macros.render_field(form.category_id) }} 55 |
56 |
57 | {{ macros.render_field(form.expected_rc) }} 58 |
59 |
60 | {{ macros.render_field(form.submit) }} 61 |
62 |
63 |
64 | {% endblock %} 65 | -------------------------------------------------------------------------------- /templates/regression/test_delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Test Delete {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
Test Delete
10 |

Are you sure you want to delete this? This change is irreversible!

11 |
12 |
13 |
14 | {{ form.csrf_token }} 15 | {% with messages = get_flashed_messages(with_categories=true) %} 16 | {% if messages %} 17 |
18 |
19 |
    20 | {% for category, message in messages %} 21 |
  • {{ message }}
  • 22 | {% endfor %} 23 |
24 |
25 |
26 | {% endif %} 27 | {% endwith %} 28 |
29 | 32 |
33 | {{ macros.render_field(form.submit) }} 34 |
35 |
36 |
37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /templates/regression/test_edit.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Regression Test {{ regression_id }} Edit {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |

Regression Test Edit

10 |

Editing regression test with id {{ regression_id }}

11 |
12 | {{ form.csrf_token }} 13 | {% with messages = get_flashed_messages(with_categories=true) %} 14 | {% if messages %} 15 |
16 |
17 |
    18 | {% for category, message in messages %} 19 |
  • {{ message }}
  • 20 | {% endfor %} 21 |
22 |
23 |
24 | {% endif %} 25 | {% endwith %} 26 | {% if form.errors %} 27 |
28 |
29 | {% for field, error in form.errors.items() %} 30 | {% for e in error %} 31 | {{ e }}
32 | {% endfor %} 33 | {% endfor %} 34 |
35 |
36 | {% endif %} 37 |
38 |
39 | {{ macros.render_field(form.sample_id) }} 40 |
41 |
42 | {{ macros.render_field(form.command) }} 43 |
44 |
45 | {{ macros.render_field(form.description) }} 46 |
47 |
48 | {{ macros.render_field(form.input_type) }} 49 |
50 |
51 | {{ macros.render_field(form.output_type) }} 52 |
53 |
54 | {{ macros.render_field(form.category_id) }} 55 |
56 |
57 | {{ macros.render_field(form.expected_rc) }} 58 |
59 |
60 | {{ macros.render_field(form.submit) }} 61 |
62 |
63 |
64 |
65 |
66 |
Actions
67 | 70 |
71 |
72 | {% endblock %} 73 | -------------------------------------------------------------------------------- /templates/regression/test_view.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Regression test {{ test.id }} {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |

Regression test {{ test.id }}

10 |

Sample: Sample #{{ test.sample.id }}

11 |

Command: {{ test.command }}

12 |

Input type: {{ test.input_type.description }}

13 |

Output type: {{ test.output_type.description }}

14 |

Description: {{ test.description or "No description" }}

15 |

16 | {% set sample = test.sample %} 17 | Tags of sample: 18 | {% if sample.tags|length %} 19 | {% for tag in sample.tags %} 20 | {{ tag.name }} 21 | {% endfor %} 22 | {% else %} 23 | No tags yet. 24 | {% endif %} 25 | {% if user.is_admin %} 26 |   27 | {% endif %} 28 |

29 |

Output files:

30 | 42 |
43 |
44 | {% if user is not none and user.has_role('contributor') %} 45 |
Regression tests
46 | 50 | {% endif %} 51 |
52 |
53 | {% endblock %} 54 | -------------------------------------------------------------------------------- /templates/sample/delete_sample.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Delete sample {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
10 |

Delete sample

11 |

Are you sure you want to delete the sample with id {{ sample.id }}?

12 |

This will delete the files, as well as all tests that were created for this sample!

13 |
14 | {{ form.csrf_token }} 15 |
16 |
17 | {{ macros.render_field(form.submit) }} 18 |
19 |
20 | 21 |
22 |
23 |
24 |
25 |
26 |
27 | {% endblock %} -------------------------------------------------------------------------------- /templates/sample/delete_sample_additional.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Delete sample {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
10 |

Delete sample extra file

11 |

Are you sure you want to delete the extra file (id: {{ extra.id }}) from sample {{ sample.id }}?

12 |

This will delete the file on the server!

13 |
14 | {{ form.csrf_token }} 15 |
16 |
17 | {{ macros.render_field(form.submit) }} 18 |
19 |
20 | 21 |
22 |
23 |
24 |
25 |
26 |
27 | {% endblock %} -------------------------------------------------------------------------------- /templates/sample/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Sample information {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
10 |

Sample information

11 |

Please select one of the samples below to see more detailed information about it.

12 | {% include "sample/list_samples.html" %} 13 |
14 |
15 | 16 |
17 | {% endblock %} -------------------------------------------------------------------------------- /templates/sample/list_issues.html: -------------------------------------------------------------------------------- 1 | {% if issues == 'ERROR' %} 2 |

Error Fetching Data from GitHub. Try Again Later.

3 | {% elif issues|length > 0 %} 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% for issue in issues %} 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% endfor %} 23 | 24 |
IssueCreated ByCreated AtStatus
{{ issue.title }} #{{ issue.issue_id }} {{ issue.user }}{{ issue.created_at|date('%Y-%m-%d %H:%M:%S (UTC)') }}{{ issue.status}}
25 |
26 | {% else %} 27 |

There are no issues available right now! Please check again later.

28 | {% endif %} 29 | -------------------------------------------------------------------------------- /templates/sample/list_samples.html: -------------------------------------------------------------------------------- 1 | {% if samples|length >= 1 %} 2 |

Samples that have been submitted in the past:

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {% for sample in samples %} 14 | 15 | 16 | 17 | 24 | 35 | 36 | {% endfor %} 37 | 38 |
{{ 'Original name' if use_sample_original|default(false) else 'SHA-256' }}Video-stream typeActionsTags
{{ sample.original_name if use_sample_original|default(false) else sample.sha }}{{ sample.extension }} 18 | 19 | {% if user.is_admin %} 20 |   21 |   22 | {% endif %} 23 | 25 | {% set visible_tags_count = 2 %} 26 | {% for tag in sample.tags[:visible_tags_count] %} 27 | {{ tag.name }} 28 | {% endfor %} 29 | {% if sample.tags|length > visible_tags_count %} 30 | 31 | +{{ sample.tags|length - visible_tags_count }} more 32 | 33 | {% endif %} 34 |
39 | {% else %} 40 |

{{ no_samples|default('Sorry, there are no samples available yet. Please return later.') }}

41 | {% endif %} -------------------------------------------------------------------------------- /templates/sample/sample_not_found.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Sample not found {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |

Sample not found

10 |

{{ message|default('The sample you requested could not be found.') }}

11 |

If you think this is an error, please get in touch.

12 |
13 |
14 | {% endblock %} -------------------------------------------------------------------------------- /templates/test/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Latest tests {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
10 |

Latest tests

11 |

Below you can see the overview of the last {{ tests|length }} test runs that were performed:

12 | 13 |   17 | {% include "test/list_table.html" %} 18 |
19 |
20 | 21 |
22 | {% endblock %} 23 | {% block scripts %} 24 | {{ super() }} 25 | 45 | {% endblock %} 46 | -------------------------------------------------------------------------------- /templates/test/list_table.html: -------------------------------------------------------------------------------- 1 | {% if tests|length > 0 %} 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {% for test in tests %} 15 | 16 | 17 | 18 | 19 | 20 | 29 | 30 | {% endfor %} 31 | 32 |
RepositoryDetailsPlatformRun errorsCompleted
{{ test.fork.github_name }}{{ test.test_type.description }} {{ (test.commit[:7] if test.test_type == TestType.commit else test.pr_nr) }}{{ test.platform.description }}{{ "Unknown" if not test.finished else ("Yes" if test.failed else "No") }} 21 | {{ "Yes" if test.finished else "No" }} 22 | {% if user.is_admin or customize %} 23 |   24 | {% if not test.finished %} 25 |   26 | {% endif %} 27 | {% endif %} 28 |
33 |
34 | {% else %} 35 |

There are no tests available right now! Please check again later.

36 | {% endif %} -------------------------------------------------------------------------------- /templates/test/test_not_found.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Test not found {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |

Test not found

10 |

{{ message|default('The test you requested could not be found.') }}

11 |

If you think this is an error, please get in touch.

12 |
13 |
14 | {% endblock %} -------------------------------------------------------------------------------- /templates/upload/delete_id.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Delete queued file {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
10 |

Delete queued file

11 |

Are you sure you want to delete the queued file with id {{ queued_sample.id }} (filename: {{ queued_sample.original_name ~ queued_sample.extension }})?

12 |

This will delete the files, as well as all tests that were created for this sample!

13 |
14 | {{ form.csrf_token }} 15 |
16 |
17 | {{ macros.render_field(form.submit) }} 18 |
19 |
20 | 21 |
22 |
23 |
24 |
25 |
26 |
27 | {% endblock %} -------------------------------------------------------------------------------- /templates/upload/filezilla_template.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ host }} 6 | {{ port }} 7 | 6 8 | 0 9 | {{ username }} 10 | {{ password }} 11 | 1 12 | 0 13 | MODE_DEFAULT 14 | 0 15 | Auto 16 | 0 17 | CCExtractor CI Platform 18 | 19 | 20 | 21 | 0 22 | 0 23 | 24 | 25 | -------------------------------------------------------------------------------- /templates/upload/ftp_index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Sample upload through FTP {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |

Upload a new sample through FTP

10 |

Below you can find the login details so you can upload your sample.

11 |

These credentials are for you alone! This will guarantee your privacy. If you share them, your samples are no longer guaranteed to be viewable by you alone.

12 |
13 |
Server:
{{ host }}
14 |
Port:
{{ port }}
15 |
Username:
{{ username }}
16 |
Password:
{{ password }}
17 |
18 |
19 | As soon as the server detects that the file was completely uploaded, the sample will be processed, which moves it to another location. You can see a list of the successfully uploaded files at the upload page. 20 |
21 |
22 |
23 |

Pre-configured configuration files

24 |

For some programs we offer a ready-made configuration, which you only need to import in order to be able to connect to the platform's FTP site.

25 |

FileZilla

26 |
27 |
28 |
    29 |
  1. Download the FileZilla configuration file
  2. 30 |
  3. Open FileZilla
  4. 31 |
  5. Open the configuration file using File -> Import
  6. 32 |
  7. Confirm you want to import site entries
  8. 33 |
  9. You should now have an entry called "Sample Submission Platform", to which you can connect to
  10. 34 |
35 |
36 |
37 | 38 |
39 |
40 |

Other clients

41 |

At this moment, there are no automated configuration files yet available for other clients. You can connect to the server using the credentials above.

42 | {# FUTURE: add auto-generated files for other clients #} 43 |
44 |
45 | {% endblock %} -------------------------------------------------------------------------------- /templates/upload/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Upload a new sample {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |

Upload a new sample

10 |

You can upload a new sample in two ways onto the platform:

11 | 15 |

After you upload them, they are added into the queue below, from where you can add the necessary details in order to submit the sample.

16 | {% include "upload/process_table.html" %} 17 |
18 |
19 | {% endblock %} -------------------------------------------------------------------------------- /templates/upload/index_admin.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Upload a new sample {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |

All currently queued samples/messages

10 | {% set admin = true %} 11 | {% include "upload/process_table.html" %} 12 |
13 |
14 | {% endblock %} -------------------------------------------------------------------------------- /templates/upload/link_id.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Link queued file to sample {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |
10 |

Link queued file to sample

11 |

This will link the submitted file (filename: {{ queued_sample.original_name ~ queued_sample.extension }}) to a sample you submitted previously.

12 | {% if samples|length >= 1 %} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% for sample in samples %} 23 | 24 | 27 | 28 | 31 | 32 | {% endfor %} 33 | 34 |
Original nameExtensionActions
25 | {{ sample.original_name }} 26 | {{ sample.extension }} 29 | Link to this sample 30 |
35 | {% else %} 36 |

Sorry, but you need to submit a sample first before you can link anything to it...

37 | {% endif %} 38 |
39 |
40 |
41 | {% endblock %} -------------------------------------------------------------------------------- /templates/upload/process_table.html: -------------------------------------------------------------------------------- 1 |

Queued samples

2 | {% if queue|length > 0 %} 3 | 4 | 5 | 6 | 7 | 8 | {% if admin|default(false) %} 9 | 10 | {% endif %} 11 | 12 | 13 | 14 | 15 | {% for entry in queue %} 16 | 17 | 18 | 19 | {% if admin|default(false) %} 20 | 23 | {% endif %} 24 | 31 | 32 | {% endfor %} 33 | 34 |
NameExtensionUserActions
{{ entry.original_name | filename }}{{ entry.extension }} 21 | {{ entry.user.name }} 22 | 25 | {% if not admin|default(false) %} 26 |   27 |   28 | {% endif %} 29 | 30 |
35 | {% else %} 36 |

There are no queued samples.

37 | {% endif %} 38 | {% if messages|length > 0 %} 39 |

Error messages

40 |

This are the last 10 error messages that occurred during processing uploaded files

41 |
    42 | {% for entry in messages %} 43 |
  • 44 |   45 | {{ entry.message }}  46 | {% if admin|default(false) %} 47 | (user: {{ entry.user.name }}) 48 | {% endif %} 49 |
  • 50 | {% endfor %} 51 |
52 | {% endif %} -------------------------------------------------------------------------------- /templates/upload/queued_sample_not_found.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Queued sample not found {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |

Queued sample not found

10 |

{{ message|default('The queued sample you requested could not be found.') }}

11 |

If you think this is an error, please get in touch.

12 |
13 |
14 | {% endblock %} -------------------------------------------------------------------------------- /templates/upload/upload.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Upload a new sample {{ super() }}{% endblock %} 4 | {% block body %} 5 | {{ super() }} 6 |
7 |
8 |
9 |

Upload a new sample

10 |

Please select a file using the "browse" button below, and then press the "Upload file" button. This can take a while though! For bigger files it's recommended to use FTP upload instead.

11 |

The maximum allowed upload size is: {{ upload_size }} MB

12 |
13 | {{ form.csrf_token }} 14 | {% with messages = get_flashed_messages(with_categories=true) %} 15 | {% if messages %} 16 |
17 |
18 |
    19 | {% for category, message in messages %} 20 |
  • {{ message }}
  • 21 | {% endfor %} 22 |
23 |
24 |
25 | {% endif %} 26 | {% endwith %} 27 | {% if form.errors %} 28 |
29 |
30 | {% for field, error in form.errors.items() %} 31 | {% for e in error %} 32 | {{ e }}
33 | {% endfor %} 34 | {% endfor %} 35 |
36 |
37 | {% endif %} 38 |
39 | {{ macros.render_field(form.file) }} 40 |
41 |
42 | {{ macros.render_field(form.submit) }} 43 |
44 |
45 |
46 |
47 |
48 |
49 |

Accepted files: {{ accept }}. If you want to upload another type, please make a request to get it added.

50 |
51 |
52 | {% endblock %} -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | pycodestyle==2.11.0 2 | pydocstyle==6.3.0 3 | dodgy==0.2.1 4 | isort==5.12.0 5 | mypy==1.5.1 6 | Werkzeug==1.0.1 7 | Flask-Testing==0.8.1 8 | blinker==1.6.2 9 | nose2-cov==1.0a4 10 | coverage==7.3.0 11 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains all project tests.""" 2 | -------------------------------------------------------------------------------- /tests/static_mock_files/expected.txt: -------------------------------------------------------------------------------- 1 | these 2 | are 3 | expected 4 | output 5 | these 6 | are 7 | expected 8 | output 9 | these 10 | are 11 | expected 12 | output 13 | these 14 | are 15 | expected 16 | output 17 | these 18 | are 19 | expected 20 | output 21 | these 22 | are 23 | expected 24 | output 25 | these 26 | are 27 | expected 28 | output 29 | these 30 | are 31 | expected 32 | output 33 | these 34 | are 35 | expected 36 | output 37 | these 38 | are 39 | expected 40 | output 41 | these 42 | are 43 | expected 44 | output 45 | these 46 | are 47 | expected 48 | output 49 | these 50 | are 51 | expected 52 | output 53 | these 54 | are 55 | expected 56 | output 57 | these 58 | are 59 | expected 60 | output 61 | these 62 | are 63 | expected 64 | output 65 | these 66 | are 67 | expected 68 | output -------------------------------------------------------------------------------- /tests/static_mock_files/obtained.txt: -------------------------------------------------------------------------------- 1 | result 2 | is 3 | not 4 | result 5 | is 6 | not 7 | correct 8 | result 9 | is 10 | not 11 | correct 12 | result 13 | is 14 | not 15 | correct 16 | result 17 | is 18 | not 19 | correct 20 | result 21 | is 22 | not 23 | correct 24 | result 25 | is 26 | not 27 | correct 28 | result 29 | is 30 | not 31 | correct 32 | result 33 | is 34 | not 35 | correct 36 | result 37 | is 38 | not 39 | correct 40 | result 41 | is 42 | not 43 | correct 44 | correct 45 | result 46 | is 47 | not 48 | correct 49 | result 50 | is 51 | not 52 | correct 53 | result 54 | is 55 | not 56 | correct 57 | result 58 | is 59 | not 60 | correct 61 | result 62 | is 63 | not 64 | correct 65 | result 66 | is 67 | not 68 | correct 69 | result 70 | is 71 | not 72 | correct 73 | result 74 | is 75 | not 76 | correct -------------------------------------------------------------------------------- /tests/test_auth/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains tests for mod_auth.""" 2 | -------------------------------------------------------------------------------- /tests/test_auth/test_forms.py: -------------------------------------------------------------------------------- 1 | from flask import g 2 | from wtforms.validators import ValidationError 3 | 4 | from mod_auth.forms import unique_username, valid_password 5 | from mod_auth.models import User 6 | from tests.base import BaseTestCase 7 | 8 | 9 | class Field: 10 | """Mock object for fields.""" 11 | 12 | def __init__(self, data): 13 | self.data = data 14 | 15 | 16 | class TestForm(BaseTestCase): 17 | """Test form fields validation.""" 18 | 19 | def test_unique_username(self): 20 | """Test that username is always unique.""" 21 | user = User(name="thealphadollar") 22 | g.db.add(user) 23 | g.db.commit() 24 | 25 | user_field = Field("thealphadollar") 26 | 27 | with self.assertRaises(ValidationError): 28 | unique_username(None, user_field) 29 | 30 | def test_empty_invalid_password(self): 31 | """Test validation fail for zero length password.""" 32 | pass_field = Field("") 33 | 34 | with self.assertRaises(ValidationError): 35 | valid_password(None, pass_field) 36 | 37 | def test_less_than_min_length_invalid_password(self): 38 | """Test validation fail for password of length less than min length.""" 39 | pass_field = Field("".join(['x' * (int(self.app.config['MIN_PWD_LEN']) - 1)])) 40 | 41 | with self.assertRaises(ValidationError): 42 | valid_password(None, pass_field) 43 | 44 | def test_more_than_max_length_invalid_password(self): 45 | """Test validation fail for password of length more than max length.""" 46 | pass_field = Field("".join(['x' * (int(self.app.config['MAX_PWD_LEN']) + 1)])) 47 | 48 | with self.assertRaises(ValidationError): 49 | valid_password(None, pass_field) 50 | 51 | def test_valid_password(self): 52 | """Test validation pass for valid password.""" 53 | pass_field = Field("".join(['x' * (int(self.app.config['MAX_PWD_LEN']))])) 54 | 55 | valid_password(None, pass_field) 56 | -------------------------------------------------------------------------------- /tests/test_ci/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains tests for mod_ci.""" 2 | -------------------------------------------------------------------------------- /tests/test_config_parser.py: -------------------------------------------------------------------------------- 1 | """Contains tests related to the config handling.""" 2 | 3 | import json 4 | from unittest import mock 5 | 6 | from config_parser import parse_config 7 | from tests.base import BaseTestCase, provide_file_at_root 8 | 9 | 10 | class TestConfigParser(BaseTestCase): 11 | """Test config parsing.""" 12 | 13 | def test_parse_config(self): 14 | """Test parse_config function.""" 15 | file_config = "TEST = 'run'" 16 | expected_config = {'TEST': 'run'} 17 | 18 | with provide_file_at_root('parse.py', file_config, to_delete=False): 19 | out_config = parse_config('parse') 20 | 21 | self.assertEquals(out_config, expected_config) 22 | -------------------------------------------------------------------------------- /tests/test_customized/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains tests for mod_customized.""" 2 | -------------------------------------------------------------------------------- /tests/test_home/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains tests for mod_home.""" 2 | -------------------------------------------------------------------------------- /tests/test_home/test_controllers.py: -------------------------------------------------------------------------------- 1 | from mod_auth.models import Role 2 | from tests.base import BaseTestCase 3 | 4 | 5 | class TestControllers(BaseTestCase): 6 | """Test main controllers.""" 7 | 8 | def test_root(self): 9 | """Test the access of the home page.""" 10 | response = self.app.test_client().get('/') 11 | self.assertEqual(response.status_code, 200) 12 | self.assert_template_used('home/index.html') 13 | 14 | def test_about(self): 15 | """Test the access of the about page.""" 16 | response = self.app.test_client().get('/about') 17 | self.assertEqual(response.status_code, 200) 18 | self.assert_template_used('home/about.html') 19 | 20 | def test_if_user_has_test_access_rights(self): 21 | """Test if the user will have rights to the tests.""" 22 | self.create_user_with_role( 23 | self.user.name, self.user.email, self.user.password, Role.admin) 24 | 25 | with self.app.test_client() as c: 26 | response_login = c.post( 27 | '/account/login', data=self.create_login_form_data(self.user.email, self.user.password)) 28 | 29 | response = c.get('/') 30 | self.assertEqual(response.status_code, 200) 31 | self.assert_context('test_access', True) 32 | self.assert_template_used('home/index.html') 33 | -------------------------------------------------------------------------------- /tests/test_log_configuration.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | from unittest import mock 4 | 5 | from log_configuration import LogConfiguration 6 | 7 | # This is necessary to avoid a warning with PyCharm 8 | # FIXME: This is apparently necessary to avoid PyCharm warnings, but mypy complains 9 | # about assigning to a method - type: ignore seems to work but probably ignores errors 10 | mock.patch.object = mock.patch.object # type: ignore 11 | 12 | 13 | class TestLogConfiguration(unittest.TestCase): 14 | """Test log setup.""" 15 | 16 | def _test_init_with_log_value(self, debug, result_level): 17 | """Test logger initialization with specific debug and level.""" 18 | joined_path = 'baz' 19 | folder = 'foo' 20 | filename = 'bar' 21 | console_format = '[%(levelname)s] %(message)s' 22 | file_format = '[%(name)s][%(levelname)s][%(asctime)s] %(message)s' 23 | with mock.patch('logging.handlers.RotatingFileHandler') as mock_fh: 24 | with mock.patch('logging.StreamHandler') as mock_sh: 25 | with mock.patch('logging.Formatter') as mock_formatter: 26 | with mock.patch('os.path.join', 27 | return_value=joined_path) as mock_join: 28 | log_config = LogConfiguration(folder, filename, debug) 29 | 30 | mock_sh().setLevel.assert_called_once_with( 31 | result_level) 32 | mock_sh().setFormatter.assert_called_once_with( 33 | mock_formatter()) 34 | mock_fh.assert_called_once_with(joined_path, 35 | maxBytes=1024 * 1024, 36 | backupCount=20) 37 | mock_fh().setLevel.assert_called_once_with( 38 | logging.DEBUG) 39 | mock_fh().setFormatter.assert_called_once_with( 40 | mock_formatter()) 41 | mock_formatter.assert_has_calls([ 42 | mock.call(console_format), 43 | mock.call(file_format) 44 | ]) 45 | mock_join.assert_called_once_with(folder, 'logs', 46 | '%s.log' % filename) 47 | 48 | self.assertEqual(log_config._consoleLogger, mock_sh()) 49 | self.assertEqual(log_config.console_logger, mock_sh()) 50 | self.assertEqual(log_config._fileLogger, mock_fh()) 51 | self.assertEqual(log_config.file_logger, mock_fh()) 52 | 53 | return log_config 54 | 55 | def test_init_correctly_initializes_the_instance_when_debug(self): 56 | """Test log initialization with debug mode and level.""" 57 | self._test_init_with_log_value(True, logging.DEBUG) 58 | 59 | def test_init_correctly_initializes_the_instance_when_no_debug(self): 60 | """Test log initialization with info level.""" 61 | self._test_init_with_log_value(False, logging.INFO) 62 | 63 | def test_create_logger(self): 64 | """Test logger creation.""" 65 | with mock.patch.object(LogConfiguration, '__init__', 66 | return_value=None): 67 | with mock.patch('logging.getLogger') as mock_get: 68 | with mock.patch.object(LogConfiguration, 'file_logger'): 69 | with mock.patch.object(LogConfiguration, 'console_logger'): 70 | log_config = LogConfiguration('foo', 'bar') 71 | name = 'foobar' 72 | 73 | result = log_config.create_logger(name) 74 | 75 | mock_get.assert_called_once_with(name) 76 | mock_get().setLevel.assert_called_once_with( 77 | logging.DEBUG) 78 | mock_get().addHandler.assert_has_calls([ 79 | mock.call(log_config.file_logger), 80 | mock.call(log_config.console_logger) 81 | ]) 82 | 83 | self.assertEqual(result, mock_get()) 84 | -------------------------------------------------------------------------------- /tests/test_mailer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | 4 | from mailer import Mailer 5 | 6 | 7 | class TestMailer(unittest.TestCase): 8 | """Test Mailer features.""" 9 | 10 | def test_that_init_works_correctly(self): 11 | """Test Mailer initialization.""" 12 | domain = 'domain' 13 | api_key = 'APIKEY' 14 | sender_name = 'foo' 15 | auth = ("api", api_key) 16 | api_url = "https://api.mailgun.net/v3/%s" % domain 17 | sender = "%s " % (sender_name, domain) 18 | 19 | actual = Mailer(domain, api_key, sender_name) 20 | 21 | self.assertEqual(actual.api_url, api_url) 22 | self.assertEqual(actual.auth, auth) 23 | self.assertEqual(actual.sender, sender) 24 | 25 | def test_that_send_simple_message_creates_the_appropriate_request(self): 26 | """Test simple message sending.""" 27 | domain = 'domain' 28 | api_key = 'APIKEY' 29 | sender_name = 'foo' 30 | 31 | mailer = Mailer(domain, api_key, sender_name) 32 | 33 | with mock.patch('requests.post') as mock_post: 34 | data = {'subject': 'foo'} 35 | mailer.send_simple_message(data) 36 | expected_data = data.copy() 37 | expected_data['from'] = mailer.sender 38 | 39 | mock_post.assert_called_once_with("%s/messages" % mailer.api_url, 40 | auth=mailer.auth, 41 | data=expected_data) 42 | -------------------------------------------------------------------------------- /tests/test_regression/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains tests for mod_customized.""" 2 | -------------------------------------------------------------------------------- /tests/test_run.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | from unittest import mock 4 | 5 | from tests.base import BaseTestCase, provide_file_at_root 6 | 7 | 8 | class mock_application: 9 | """Mock object for application.""" 10 | 11 | def __init__(self): 12 | self.config = {} 13 | self.root_path = '' 14 | 15 | 16 | class TestRun(BaseTestCase): 17 | """Test application running.""" 18 | 19 | def test_load_secret_keys_files_present(self): 20 | """Test csrf session and secret keys loading when they are presented.""" 21 | secrets = tempfile.NamedTemporaryFile() 22 | csrf = tempfile.NamedTemporaryFile() 23 | application = mock_application() 24 | 25 | with open(secrets.name, 'w') as f: 26 | f.write('secret') 27 | with open(csrf.name, 'w') as f: 28 | f.write('csrf') 29 | 30 | with provide_file_at_root('config.py'): 31 | from run import load_secret_keys 32 | load_secret_keys(application, secrets.name, csrf.name) 33 | 34 | secrets.close() 35 | csrf.close() 36 | 37 | self.assertEqual(application.config['SECRET_KEY'], b'secret', 'secret key not loaded properly') 38 | self.assertEqual(application.config['CSRF_SESSION_KEY'], b'csrf', 'csrf session key not loaded properly') 39 | 40 | def test_load_secret_keys_secrets_not_present(self): 41 | """Test csrf session and secret keys loading when csrf session key is not presented.""" 42 | secrets = tempfile.NamedTemporaryFile() 43 | csrf = "notAvailable" 44 | application = mock_application() 45 | 46 | with open(secrets.name, 'w') as f: 47 | f.write('secret') 48 | 49 | with provide_file_at_root('config.py'): 50 | from run import load_secret_keys 51 | with self.assertRaises(SystemExit) as cmd: 52 | load_secret_keys(application, secrets.name, csrf) 53 | 54 | secrets.close() 55 | 56 | self.assertEqual(application.config['SECRET_KEY'], b'secret', 'secret key not loaded properly') 57 | self.assertEquals(cmd.exception.code, 1, 'function exited with a wrong code') 58 | 59 | def test_load_secret_keys_csrf_not_present(self): 60 | """Test csrf session and secret keys loading when secret key is not presented.""" 61 | secrets = "notAvailable" 62 | csrf = tempfile.NamedTemporaryFile() 63 | application = mock_application() 64 | 65 | with open(csrf.name, 'w') as f: 66 | f.write('csrf') 67 | 68 | with provide_file_at_root('config.py'): 69 | from run import load_secret_keys 70 | with self.assertRaises(SystemExit) as cmd: 71 | load_secret_keys(application, secrets, csrf.name) 72 | 73 | csrf.close() 74 | 75 | self.assertEquals(cmd.exception.code, 1, 'function exited with a wrong code') 76 | self.assertEqual(application.config['CSRF_SESSION_KEY'], b'csrf', 'csrf session key not loaded properly') 77 | -------------------------------------------------------------------------------- /tests/test_sample/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains tests for mod_sample.""" 2 | -------------------------------------------------------------------------------- /tests/test_test/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains tests for mod_test.""" 2 | -------------------------------------------------------------------------------- /tests/test_test/test_diff.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | from pathlib import Path 4 | 5 | from mod_test.nicediff.diff import get_html_diff 6 | from tests.base import load_file_lines 7 | 8 | STATIC_MOCK_DIR = os.path.join(Path(__file__).parents[1], 'static_mock_files') 9 | EXPECTED_RESULT_FILE = os.path.join(STATIC_MOCK_DIR, 'expected.txt') 10 | OBTAINED_RESULT_FILE = os.path.join(STATIC_MOCK_DIR, 'obtained.txt') 11 | 12 | 13 | class TestDiff(unittest.TestCase): 14 | """Test diff-related features.""" 15 | 16 | def test_if_same_diff_generated(self): 17 | """Test the diff generation.""" 18 | expected_sub = ['1\n', '00:00:12,340 --> 00:00:15,356\n', 'May the fourth be with you!\n'] 19 | obtained_sub = ['1\n', '00:00:12,300 --> 00:00:15,356\n', 'May the twenty fourth be with you!\n'] 20 | 21 | expected_diff = """ 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
Result
Expected
32 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | 42 | 43 |
2
00:00:12,
300
--> 00:00:15,356 36 |
00:00:12,
340
--> 00:00:15,356 41 |
44 | 45 | 46 | 47 | 49 | 50 | 51 | 52 | 54 | 55 |
3
May the
twenty
fourth be with you! 48 |
May the
fourth be with you! 53 |
""" 56 | 57 | obtained_diff = get_html_diff(expected_sub, obtained_sub) 58 | 59 | self.assertEqual(expected_diff, obtained_diff) 60 | 61 | def test_if_view_limit_respected(self): 62 | """Test that in view only first 50 diffs are sent.""" 63 | expected_tr_count = 102 # 2 table rows for each diff along with 2 table rows for headings 64 | obtained = load_file_lines(OBTAINED_RESULT_FILE) 65 | expected = load_file_lines(EXPECTED_RESULT_FILE) 66 | 67 | obtained_diff = get_html_diff(expected, obtained, to_view=True) 68 | obtained_tr_count = obtained_diff.count("") 69 | 70 | self.assertEqual(expected_tr_count, obtained_tr_count) 71 | 72 | def test_if_full_diff_download(self): 73 | """Test that in download mode all diff is sent.""" 74 | limit_tr_count = 102 # 2 table rows for each diff along with 2 table rows for headings 75 | obtained = load_file_lines(OBTAINED_RESULT_FILE) 76 | expected = load_file_lines(EXPECTED_RESULT_FILE) 77 | 78 | obtained_diff = get_html_diff(expected, obtained, to_view=False) 79 | obtained_tr_count = obtained_diff.count("") 80 | 81 | self.assertGreater(obtained_tr_count, limit_tr_count) 82 | -------------------------------------------------------------------------------- /tests/test_upload/__init__.py: -------------------------------------------------------------------------------- 1 | """Contains tests for mod_upload.""" 2 | -------------------------------------------------------------------------------- /tests/test_upload/test_progress_FTP_upload.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from tests.base import BaseTestCase 4 | 5 | 6 | class TestProgressFTPUpload(BaseTestCase): 7 | """Test progress during ftp upload.""" 8 | 9 | @mock.patch('mod_upload.controllers.upload_ftp') 10 | def test_process(self, mock_upload): 11 | """Test process method.""" 12 | from mod_upload.progress_ftp_upload import process 13 | 14 | process('mock_db', 'mock_file') 15 | 16 | mock_upload.assert_called_once_with('mock_db', 'mock_file') 17 | -------------------------------------------------------------------------------- /tests/test_utility.py: -------------------------------------------------------------------------------- 1 | """Contains tests related to the utility helpers.""" 2 | 3 | from unittest import mock 4 | 5 | from tests.base import BaseTestCase, MockResponse 6 | 7 | 8 | class TestUtility(BaseTestCase): 9 | """Test utility helpers.""" 10 | 11 | @mock.patch('utility.path') 12 | def test_serve_file_download(self, mock_path): 13 | """Test function serve_file_download.""" 14 | from utility import serve_file_download 15 | 16 | response = serve_file_download('to_download', 'folder') 17 | 18 | self.assertEqual(response.status_code, 302) 19 | self.assertEqual(1, mock_path.join.call_count) 20 | 21 | @mock.patch('utility.cache_has_expired', return_value=True) 22 | @mock.patch('flask.g.log.critical') 23 | @mock.patch('requests.get', return_value=MockResponse({}, 200)) 24 | def test_get_cached_web_hook_blocks_invalid_response(self, mock_get, mock_critical, mock_is_cached_expired): 25 | """Test function get_cached_web_hook_blocks if GitHub response is invalid.""" 26 | from utility import get_cached_web_hook_blocks 27 | 28 | cached_web_hook_blocks = get_cached_web_hook_blocks() 29 | 30 | mock_get.assert_called_once() 31 | mock_critical.assert_called_once_with("Failed to retrieve hook IP's from GitHub! API returned {}") 32 | 33 | @mock.patch('flask.g.log') 34 | @mock.patch('utility.abort') 35 | @mock.patch('utility.is_github_web_hook_ip', return_value=False) 36 | @mock.patch('utility.ip_address') 37 | def test_request_from_github_invalid_request(self, mock_ip_address, mock_is_github_ip, mock_abort, mock_log): 38 | """Test request_from_github decorator when request is invalid.""" 39 | from utility import request_from_github 40 | 41 | @request_from_github() 42 | def example_function(*args, **kwargs): 43 | return "Test Success" 44 | 45 | response = example_function() 46 | 47 | mock_ip_address.assert_called_once() 48 | mock_is_github_ip.assert_called_once() 49 | self.assertEqual(mock_abort.call_count, 7) 50 | self.assertEqual(response, "Test Success") 51 | -------------------------------------------------------------------------------- /unittest.cfg: -------------------------------------------------------------------------------- 1 | [coverage] 2 | always-on = True 3 | coverage-config = .coveragerc 4 | coverage-report = xml 5 | 6 | [coverage.xml] 7 | output = coverage.xml 8 | --------------------------------------------------------------------------------