diff --git a/.coveragerc b/.coveragerc index 69c153ce..f732c9c0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,7 +3,7 @@ disable_warnings = already-imported [report] include = aurweb/* -fail_under = 85 +fail_under = 95 exclude_lines = if __name__ == .__main__.: pragma: no cover diff --git a/.dockerignore b/.dockerignore index 6ec5547d..56ac1964 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,23 @@ -*/*.mo +# Config files conf/config conf/config.sqlite conf/config.sqlite.defaults conf/docker conf/docker.defaults + +# Compiled translation files +**/*.mo + +# Typical virtualenv directories +env/ +venv/ +.venv/ + +# Test output +htmlcov/ +test-emails/ +test/__pycache__ +test/test-results +test/trash_directory* +.coverage +.pytest_cache diff --git a/.editorconfig b/.editorconfig index 5a751aad..1a8f5d77 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,5 +1,5 @@ # EditorConfig configuration for aurweb -# https://EditorConfig.org +# https://editorconfig.org # Top-most EditorConfig file root = true @@ -8,6 +8,3 @@ root = true end_of_line = lf insert_final_newline = true charset = utf-8 - -[*.{php,t}] -indent_style = tab diff --git a/.env b/.env index 22846cb4..bf6c48c4 100644 --- a/.env +++ b/.env @@ -1,7 +1,6 @@ FASTAPI_BACKEND="uvicorn" FASTAPI_WORKERS=2 MARIADB_SOCKET_DIR="/var/run/mysqld/" -AURWEB_PHP_PREFIX=https://localhost:8443 AURWEB_FASTAPI_PREFIX=https://localhost:8444 AURWEB_SSHD_PREFIX=ssh://aur@localhost:2222 GIT_DATA_DIR="./aur.git/" diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..d3c9887b --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# style: Run pre-commit +9c6c13b78a30cb9d800043410799e29631f803d2 diff --git a/.gitignore b/.gitignore index 8388694c..97157118 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/data/ __pycache__/ *.py[cod] .vim/ @@ -23,7 +24,6 @@ conf/docker conf/docker.defaults data.sql dummy-data.sql* -env/ fastapi_aw/ htmlcov/ po/*.mo @@ -31,7 +31,7 @@ po/*.po~ po/POTFILES schema/aur-schema-sqlite.sql test/test-results/ -test/trash directory* +test/trash_directory* web/locale/*/ web/html/*.gz @@ -43,3 +43,21 @@ doc/rpc.html # Ignore .python-version file from Pyenv .python-version + +# Ignore coverage report +coverage.xml + +# Ignore pytest report +report.xml + +# Ignore test emails +test-emails/ + +# Ignore typical virtualenv directories +env/ +venv/ +.venv/ + +# Ignore some terraform files +/ci/tf/.terraform +/ci/tf/terraform.tfstate* diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c5554e92..f30994c7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -4,6 +4,8 @@ cache: paths: # For some reason Gitlab CI only supports storing cache/artifacts in a path relative to the build directory - .pkg-cache + - .venv + - .pre-commit variables: AUR_CONFIG: conf/config # Default MySQL config setup in before_script. @@ -11,28 +13,27 @@ variables: TEST_RECURSION_LIMIT: 10000 CURRENT_DIR: "$(pwd)" LOG_CONFIG: logging.test.conf + DEV_FQDN: aurweb-$CI_COMMIT_REF_SLUG.sandbox.archlinux.page + INFRASTRUCTURE_REPO: https://gitlab.archlinux.org/archlinux/infrastructure.git lint: - variables: - # Space-separated list of directories that should be linted. - REQUIRES_LINT: "aurweb test migrations" stage: .pre before_script: - - pacman -Sy --noconfirm --noprogressbar --cachedir .pkg-cache + - pacman -Sy --noconfirm --noprogressbar archlinux-keyring - - pacman -Syu --noconfirm --noprogressbar --cachedir .pkg-cache - python python-isort flake8 + - pacman -Syu --noconfirm --noprogressbar + git python python-pre-commit script: - - bash -c 'flake8 --count $(echo "$REQUIRES_LINT" | xargs); exit $?' - - bash -c 'isort --check-only $(echo "$REQUIRES_LINT" | xargs); exit $?' + - export XDG_CACHE_HOME=.pre-commit + - pre-commit run -a test: stage: test - tags: - - fast-single-thread before_script: - export PATH="$HOME/.poetry/bin:${PATH}" - ./docker/scripts/install-deps.sh + - virtualenv -p python3 .venv + - source .venv/bin/activate # Enable our virtualenv cache - ./docker/scripts/install-python-deps.sh - useradd -U -d /aurweb -c 'AUR User' aur - ./docker/mariadb-entrypoint.sh @@ -48,42 +49,113 @@ test: # Run sharness. - make -C test sh # Run pytest. - - pytest + - pytest --junitxml="pytest-report.xml" - make -C test coverage # Produce coverage reports. - coverage: '/TOTAL.*\s+(\d+\%)/' + coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' artifacts: reports: - cobertura: coverage.xml + junit: pytest-report.xml + coverage_report: + coverage_format: cobertura + path: coverage.xml -deploy: +.init_tf: &init_tf + - pacman -Syu --needed --noconfirm terraform + - export TF_VAR_name="aurweb-${CI_COMMIT_REF_SLUG}" + - TF_ADDRESS="${CI_API_V4_URL}/projects/${TF_STATE_PROJECT}/terraform/state/${CI_COMMIT_REF_SLUG}" + - cd ci/tf + - > + terraform init \ + -backend-config="address=${TF_ADDRESS}" \ + -backend-config="lock_address=${TF_ADDRESS}/lock" \ + -backend-config="unlock_address=${TF_ADDRESS}/lock" \ + -backend-config="username=x-access-token" \ + -backend-config="password=${TF_STATE_GITLAB_ACCESS_TOKEN}" \ + -backend-config="lock_method=POST" \ + -backend-config="unlock_method=DELETE" \ + -backend-config="retry_wait_min=5" + +deploy_review: stage: deploy - tags: - - secure - rules: - - if: $CI_COMMIT_BRANCH == "pu" - when: manual - variables: - FASTAPI_BACKEND: gunicorn - FASTAPI_WORKERS: 5 - AURWEB_PHP_PREFIX: https://aur-dev.archlinux.org - AURWEB_FASTAPI_PREFIX: https://aur-dev.archlinux.org - AURWEB_SSHD_PREFIX: ssh://aur@aur-dev.archlinux.org:2222 - COMMIT_HASH: $CI_COMMIT_SHA - GIT_DATA_DIR: git_data script: - - pacman -Syu --noconfirm docker docker-compose socat openssh - - chmod 600 ${SSH_KEY} - - socat "UNIX-LISTEN:/tmp/docker.sock,reuseaddr,fork" EXEC:"ssh -o UserKnownHostsFile=${SSH_KNOWN_HOSTS} -Ti ${SSH_KEY} ${SSH_USER}@${SSH_HOST}" & - - export DOCKER_HOST="unix:///tmp/docker.sock" - # Set secure login config for aurweb. - - sed -ri "s/^(disable_http_login).*$/\1 = 1/" conf/config.dev - - docker-compose build - - docker-compose -f docker-compose.yml -f docker-compose.aur-dev.yml down --remove-orphans - - docker-compose -f docker-compose.yml -f docker-compose.aur-dev.yml up -d - - docker image prune -f - - docker container prune -f - - docker volume prune -f - + - *init_tf + - terraform apply -auto-approve environment: - name: development - url: https://aur-dev.archlinux.org + name: review/$CI_COMMIT_REF_NAME + url: https://$DEV_FQDN + on_stop: stop_review + auto_stop_in: 1 week + rules: + - if: $CI_COMMIT_REF_NAME =~ /^renovate\// + when: never + - if: $CI_MERGE_REQUEST_ID && $CI_PROJECT_PATH == "archlinux/aurweb" + when: manual + +provision_review: + stage: deploy + needs: + - deploy_review + script: + - *init_tf + - pacman -Syu --noconfirm --needed ansible git openssh jq + # Get ssh key from terraform state file + - mkdir -p ~/.ssh + - chmod 700 ~/.ssh + - terraform show -json | + jq -r '.values.root_module.resources[] | + select(.address == "tls_private_key.this") | + .values.private_key_openssh' > ~/.ssh/id_ed25519 + - chmod 400 ~/.ssh/id_ed25519 + # Clone infra repo + - git clone $INFRASTRUCTURE_REPO + - cd infrastructure + # Remove vault files + - rm $(git grep -l 'ANSIBLE_VAULT;1.1;AES256$') + # Remove vault config + - sed -i '/^vault/d' ansible.cfg + # Add host config + - mkdir -p host_vars/$DEV_FQDN + - 'echo "filesystem: btrfs" > host_vars/$DEV_FQDN/misc' + # Add host + - echo "$DEV_FQDN" > hosts + # Add our pubkey and hostkeys + - ssh-keyscan $DEV_FQDN >> ~/.ssh/known_hosts + - ssh-keygen -f ~/.ssh/id_ed25519 -y > pubkeys/aurweb-dev.pub + # Run our ansible playbook + - > + ansible-playbook playbooks/aur-dev.archlinux.org.yml \ + -e "aurdev_fqdn=$DEV_FQDN" \ + -e "aurweb_repository=$CI_REPOSITORY_URL" \ + -e "aurweb_version=$CI_COMMIT_SHA" \ + -e "{\"vault_mariadb_users\":{\"root\":\"aur\"}}" \ + -e "vault_aurweb_db_password=aur" \ + -e "vault_aurweb_gitlab_instance=https://does.not.exist" \ + -e "vault_aurweb_error_project=set-me" \ + -e "vault_aurweb_error_token=set-me" \ + -e "vault_aurweb_secret=aur" \ + -e "vault_goaurrpc_metrics_token=aur" \ + -e '{"root_additional_keys": ["moson.pub", "aurweb-dev.pub"]}' + environment: + name: review/$CI_COMMIT_REF_NAME + action: access + rules: + - if: $CI_COMMIT_REF_NAME =~ /^renovate\// + when: never + - if: $CI_MERGE_REQUEST_ID && $CI_PROJECT_PATH == "archlinux/aurweb" + +stop_review: + stage: deploy + needs: + - deploy_review + script: + - *init_tf + - terraform destroy -auto-approve + - 'curl --silent --show-error --fail --header "Private-Token: ${TF_STATE_GITLAB_ACCESS_TOKEN}" --request DELETE "${CI_API_V4_URL}/projects/${TF_STATE_PROJECT}/terraform/state/${CI_COMMIT_REF_SLUG}"' + environment: + name: review/$CI_COMMIT_REF_NAME + action: stop + rules: + - if: $CI_COMMIT_REF_NAME =~ /^renovate\// + when: never + - if: $CI_MERGE_REQUEST_ID && $CI_PROJECT_PATH == "archlinux/aurweb" + when: manual diff --git a/.gitlab/issue_templates/Account Request.md b/.gitlab/issue_templates/Account Request.md deleted file mode 100644 index 6831d3ad..00000000 --- a/.gitlab/issue_templates/Account Request.md +++ /dev/null @@ -1,14 +0,0 @@ -## Checklist - -- [ ] I have set a Username in the Details section -- [ ] I have set an Email in the Details section -- [ ] I have set a valid Account Type in the Details section - -## Details - -- Instance: aur-dev.archlinux.org -- Username: the_username_you_want -- Email: valid@email.org -- Account Type: (User|Trusted User) - -/label account-request diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md index 3e7a04bf..9e20aadb 100644 --- a/.gitlab/issue_templates/Bug.md +++ b/.gitlab/issue_templates/Bug.md @@ -1,12 +1,24 @@ + +/label ~bug ~unconfirmed +/title [BUG] + + ### Checklist -This bug template is meant to provide bug issues for code existing in -the aurweb repository. This bug template is **not meant** to handle -bugs with user-uploaded packages. +**NOTE:** This bug template is meant to provide bug issues for code existing in +the aurweb repository. -To work out a bug you have found in a user-uploaded package, contact -the package's maintainer first. If you receive no response, file the -relevant package request against it so TUs can deal with cleanup. +**This bug template is not meant to handle bugs with user-uploaded packages.** +To report issues you might have found in a user-uploaded package, contact +the package's maintainer in comments. - [ ] I confirm that this is an issue with aurweb's code and not a user-uploaded package. @@ -29,7 +41,7 @@ this bug. ### Logs -If you have any logs relevent to the bug, include them here in +If you have any logs relevant to the bug, include them here in quoted or code blocks. ### Version(s) diff --git a/.gitlab/issue_templates/Feature.md b/.gitlab/issue_templates/Feature.md index c907adcd..630c53c3 100644 --- a/.gitlab/issue_templates/Feature.md +++ b/.gitlab/issue_templates/Feature.md @@ -1,3 +1,25 @@ + +/label ~feature ~unconfirmed +/title [FEATURE] + + +### Checklist + +**NOTE:** This bug template is meant to provide bug issues for code existing in +the aurweb repository. + +**This bug template is not meant to handle bugs with user-uploaded packages.** +To report issues you might have found in a user-uploaded package, contact +the package's maintainer in comments. + - [ ] I have summed up the feature in concise words in the [Summary](#summary) section. - [ ] I have completely described the feature in the [Description](#description) section. - [ ] I have completed the [Blockers](#blockers) section. @@ -28,5 +50,3 @@ Example: - [Feature] Do not allow users to be Tyrants - \<(issue|merge_request)_link\> - -/label feature unconsidered diff --git a/.gitlab/issue_templates/Feedback.md b/.gitlab/issue_templates/Feedback.md deleted file mode 100644 index 950ec0c6..00000000 --- a/.gitlab/issue_templates/Feedback.md +++ /dev/null @@ -1,58 +0,0 @@ -**NOTE:** This issue template is only applicable to FastAPI implementations -in the code-base, which only exists within the `pu` branch. If you wish to -file an issue for the current PHP implementation of aurweb, please file a -standard issue prefixed with `[Bug]` or `[Feature]`. - - -**Checklist** - -- [ ] I have prefixed the issue title with `[Feedback]` along with a message - pointing to the route or feature tested. - - Example: `[Feedback] /packages/{name}` -- [ ] I have completed the [Changes](#changes) section. -- [ ] I have completed the [Bugs](#bugs) section. -- [ ] I have completed the [Improvements](#improvements) section. -- [ ] I have completed the [Summary](#summary) section. - -### Changes - -Please describe changes in user experience when compared to the PHP -implementation. This section can actually hold a lot of info if you -are up for it -- changes in routes, HTML rendering, back-end behavior, -etc. - -If you cannot see any changes from your standpoint, include a short -statement about that fact. - -### Bugs - -Please describe any bugs you've experienced while testing the route -pertaining to this issue. A "perfect" bug report would include your -specific experience, what you expected to occur, and what happened -otherwise. If you can, please include output of `docker-compose logs fastapi` -with your report; especially if any unintended exceptions occurred. - -### Improvements - -If you've experienced improvements in the route when compared to PHP, -please do include those here. We'd like to know if users are noticing -these improvements and how they feel about them. - -There are multiple routes with no improvements. For these, just include -a short sentence about the fact that you've experienced none. - -### Summary - -First: If you've gotten here and completed the [Changes](#changes), -[Bugs](#bugs), and [Improvements](#improvements) sections, we'd like -to thank you very much for your contribution and willingness to test. -We are not a company, and we are not a large team; any bit of assistance -here helps the project astronomically and moves us closer toward a -new release. - -That being said: please include an overall summary of your experience -and how you felt about the current implementation which you're testing -in comparison with PHP (current aur.archlinux.org, or https://localhost:8443 -through docker). - -/label feedback diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 525c7eb8..2431eb3d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,24 +1,36 @@ -hooks: - - &base - language: python - types: [python] - require_serial: true - exclude: ^migrations/versions - - &flake8 - id: flake8 - name: flake8 - entry: flake8 - <<: *base - - &isort - id: isort - name: isort - entry: isort - <<: *base - repos: - - repo: local + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 hooks: - - <<: *flake8 - - <<: *isort - args: ['--check-only', '--diff'] + - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict + - id: check-toml + - id: end-of-file-fixer + - id: trailing-whitespace + exclude: ^po/ + - id: debug-statements + - repo: https://github.com/myint/autoflake + rev: v2.3.1 + hooks: + - id: autoflake + args: + - --in-place + - --remove-all-unused-imports + - --ignore-init-module-imports + + - repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + + - repo: https://github.com/psf/black + rev: 24.4.1 + hooks: + - id: black + + - repo: https://github.com/PyCQA/flake8 + rev: 7.0.0 + hooks: + - id: flake8 diff --git a/.tx/config b/.tx/config index e986f81c..9ba46244 100644 --- a/.tx/config +++ b/.tx/config @@ -1,7 +1,7 @@ [main] -host = https://www.transifex.com +host = https://app.transifex.com -[aurweb.aurwebpot] +[o:lfleischer:p:aurweb:r:aurwebpot] file_filter = po/.po source_file = po/aurweb.pot source_lang = en diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2bb840f5..1957ae22 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,8 +8,8 @@ Before sending patches, you are recommended to run `flake8` and `isort`. You can add a git hook to do this by installing `python-pre-commit` and running `pre-commit install`. -[1]: https://lists.archlinux.org/listinfo/aur-dev -[2]: https://gitlab.archlinunx.org/archlinux/aurweb +[1]: https://lists.archlinux.org/mailman3/lists/aur-dev.lists.archlinux.org/ +[2]: https://gitlab.archlinux.org/archlinux/aurweb ### Coding Guidelines @@ -23,6 +23,83 @@ development. 3. Use four space indentation 4. Use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) 5. DRY: Don't Repeat Yourself -6. All code should be tested for good _and_ bad cases +6. All code should be tested for good _and_ bad cases (see [test/README.md][3]) + +[3]: https://gitlab.archlinux.org/archlinux/aurweb/-/blob/master/test/README.md Test patches that increase coverage in the codebase are always welcome. + +### Coding Style + +We use `autoflake`, `isort`, `black` and `flake8` to enforce coding style in a +PEP-8 compliant way. These tools run in GitLab CI using `pre-commit` to verify +that any pushed code changes comply with this. + +To enable the `pre-commit` git hook, install the `pre-commit` package either +with `pacman` or `pip` and then run `pre-commit install --install-hooks`. This +will ensure formatting is done before any code is commited to the git +repository. + +There are plugins for editors or IDEs which automate this process. Some +example plugins: + +- [tenfyzhong/autoflake.vim](https://github.com/tenfyzhong/autoflake.vim) +- [fisadev/vim-isort](https://github.com/fisadev/vim-isort) +- [psf/black](https://github.com/psf/black) +- [nvie/vim-flake8](https://github.com/nvie/vim-flake8) +- [prabirshrestha/vim-lsp](https://github.com/prabirshrestha/vim-lsp) +- [dense-analysis/ale](https://github.com/dense-analysis/ale) + +See `setup.cfg`, `pyproject.toml` and `.pre-commit-config.yaml` for tool +specific configurations. + +### Development Environment + +To get started with local development, an instance of aurweb must be +brought up. This can be done using the following sections: + +- [Using Docker](#using-docker) +- [Using INSTALL](#using-install) + +There are a number of services aurweb employs to run the application +in its entirety: + +- ssh +- cron jobs +- starlette/fastapi asgi server + +Project structure: + +- `./aurweb`: `aurweb` Python package +- `./templates`: Jinja2 templates +- `./docker`: Docker scripts and configuration files + +#### Using Docker + +Using Docker, we can run the entire infrastructure in two steps: + + # Build the aurweb:latest image + $ docker-compose build + + # Start all services in the background + $ docker-compose up -d nginx + +`docker-compose` services will generate a locally signed root certificate +at `./data/root_ca.crt`. Users can import this into ca-certificates or their +browser if desired. + +Accessible services (on the host): + +- https://localhost:8444 (python via nginx) +- localhost:13306 (mariadb) +- localhost:16379 (redis) + +Docker services, by default, are setup to be hot reloaded when source code +is changed. + +For detailed setup instructions have a look at [TESTING](TESTING) + +#### Using INSTALL + +The [INSTALL](INSTALL) file describes steps to install the application on +bare-metal systems. diff --git a/Dockerfile b/Dockerfile index 16e6514e..1f667611 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,10 +2,12 @@ FROM archlinux:base-devel VOLUME /root/.cache/pypoetry/cache VOLUME /root/.cache/pypoetry/artifacts +VOLUME /root/.cache/pre-commit ENV PATH="/root/.poetry/bin:${PATH}" ENV PYTHONPATH=/aurweb ENV AUR_CONFIG=conf/config +ENV COMPOSE=1 # Install system-wide dependencies. COPY ./docker/scripts/install-deps.sh /install-deps.sh @@ -27,7 +29,7 @@ RUN cp -vf conf/config.dev conf/config RUN sed -i "s;YOUR_AUR_ROOT;/aurweb;g" conf/config # Install Python dependencies. -RUN /docker/scripts/install-python-deps.sh +RUN /docker/scripts/install-python-deps.sh compose # Compile asciidocs. RUN make -C doc @@ -40,3 +42,6 @@ RUN ln -sf /usr/share/zoneinfo/UTC /etc/localtime # Install translations. RUN make -C po all install + +# Install pre-commit repositories and run lint check. +RUN pre-commit run -a diff --git a/INSTALL b/INSTALL index 03459726..23fb6c3d 100644 --- a/INSTALL +++ b/INSTALL @@ -14,8 +14,7 @@ read the instructions below. $ cd aurweb $ poetry install -2) Setup a web server with PHP and MySQL. Configure the web server to redirect - all URLs to /index.php/foo/bar/. The following block can be used with nginx: +2) Setup a web server with MySQL. The following block can be used with nginx: server { # https is preferred and can be done easily with LetsEncrypt @@ -31,14 +30,6 @@ read the instructions below. ssl_certificate /etc/ssl/certs/aur.cert.pem; ssl_certificate_key /etc/ssl/private/aur.key.pem; - # Asset root. This is used to match against gzip archives. - root /srv/http/aurweb/web/html; - - # TU Bylaws redirect. - location = /trusted-user/TUbylaws.html { - return 301 https://tu-bylaws.aur.archlinux.org; - } - # smartgit location. location ~ "^/([a-z0-9][a-z0-9.+_-]*?)(\.git)?/(git-(receive|upload)-pack|HEAD|info/refs|objects/(info/(http-)?alternates|packs)|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\.(pack|idx))$" { include uwsgi_params; @@ -63,6 +54,9 @@ read the instructions below. # Static archive assets. location ~ \.gz$ { + # Asset root. This is used to match against gzip archives. + root /srv/http/aurweb/archives; + types { application/gzip text/plain } default_type text/plain; add_header Content-Encoding gzip; @@ -126,7 +120,7 @@ interval: */2 * * * * bash -c 'poetry run aurweb-pkgmaint' */2 * * * * bash -c 'poetry run aurweb-usermaint' */2 * * * * bash -c 'poetry run aurweb-popupdate' - */12 * * * * bash -c 'poetry run aurweb-tuvotereminder' + */12 * * * * bash -c 'poetry run aurweb-votereminder' 7) Create a new database and a user and import the aurweb SQL schema: diff --git a/README.md b/README.md index 3f156455..adf78c35 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ The aurweb project includes * A web interface to search for packaging scripts and display package details. * An SSH/Git interface to submit and update packages and package meta data. * Community features such as comments, votes, package flagging and requests. -* Editing/deletion of packages and accounts by Trusted Users and Developers. -* Area for Trusted Users to post AUR-related proposals and vote on them. +* Editing/deletion of packages and accounts by Package Maintainers and Developers. +* Area for Package Maintainers to post AUR-related proposals and vote on them. Directory Layout ---------------- @@ -26,7 +26,6 @@ Directory Layout * `schema`: schema for the SQL database * `test`: test suite and test cases * `upgrading`: instructions for upgrading setups from one release to another -* `web`: PHP-based web interface for the AUR Documentation ------------- @@ -44,7 +43,7 @@ Links ----- * The repository is hosted at https://gitlab.archlinux.org/archlinux/aurweb - -- see doc/CodingGuidelines for information on the patch submission process. + -- see [CONTRIBUTING.md](./CONTRIBUTING.md) for information on the patch submission process. * Bugs can (and should) be submitted to the aurweb bug tracker: https://gitlab.archlinux.org/archlinux/aurweb/-/issues/new?issuable_template=Bug @@ -57,7 +56,7 @@ Translations ------------ Translations are welcome via our Transifex project at -https://www.transifex.com/lfleischer/aurweb; see `doc/i18n.txt` for details. +https://www.transifex.com/lfleischer/aurweb; see [doc/i18n.md](./doc/i18n.md) for details. ![Transifex](https://www.transifex.com/projects/p/aurweb/chart/image_png) diff --git a/TESTING b/TESTING index 776be2f4..e9cbf33b 100644 --- a/TESTING +++ b/TESTING @@ -1,50 +1,130 @@ Setup Testing Environment ========================= +The quickest way to get you hacking on aurweb is to utilize docker. +In case you prefer to run it bare-metal see instructions further below. + +Containerized environment +------------------------- + +1) Clone the aurweb project: + + $ git clone https://gitlab.archlinux.org/archlinux/aurweb.git + $ cd aurweb + +2) Install the necessary packages: + + # pacman -S --needed docker docker-compose + +3) Build the aurweb:latest image: + + # systemctl start docker + # docker compose build + +4) Run local Docker development instance: + + # docker compose up -d + +5) Browse to local aurweb development server. + + https://localhost:8444/ + +6) [Optionally] populate the database with dummy data: + + # docker compose exec mariadb /bin/bash + # pacman -S --noconfirm words fortune-mod + # poetry run schema/gendummydata.py dummy_data.sql + # mariadb -uaur -paur aurweb < dummy_data.sql + # exit + + Inspect `dummy_data.sql` for test credentials. + Passwords match usernames. + +We now have fully set up environment which we can start and stop with: + + # docker compose start + # docker compose stop + +Proceed with topic "Setup for running tests" + + +Bare Metal installation +----------------------- + Note that this setup is only to test the web interface. If you need to have a full aurweb instance with cgit, ssh interface, etc, follow the directions in INSTALL. -docker-compose --------------- - -1) Clone the aurweb project: - - $ git clone https://gitlab.archlinux.org/archlinux/aurweb.git - -2) Install the necessary packages: - - # pacman -S docker-compose - -2) Build the aurweb:latest image: - - $ cd /path/to/aurweb/ - $ docker-compose build - -3) Run local Docker development instance: - - $ cd /path/to/aurweb/ - $ docker-compose up -d nginx - -4) Browse to local aurweb development server. - - Python: https://localhost:8444/ - PHP: https://localhost:8443/ - -Bare Metal ----------- - 1) Clone the aurweb project: $ git clone git://git.archlinux.org/aurweb.git + $ cd aurweb 2) Install the necessary packages: - # pacman -S python-poetry + # pacman -S --needed python-poetry mariadb words fortune-mod nginx -4) Install the package/dependencies via `poetry`: +3) Install the package/dependencies via `poetry`: + + $ poetry install + +4) Copy conf/config.dev to conf/config and replace YOUR_AUR_ROOT by the absolute + path to the root of your aurweb clone. sed can do both tasks for you: + + $ sed -e "s;YOUR_AUR_ROOT;$PWD;g" conf/config.dev > conf/config + + Note that when the upstream config.dev is updated, you should compare it to + your conf/config, or regenerate your configuration with the command above. + +5) Set up mariadb: + + # mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql + # systemctl start mariadb + # mariadb -u root + > CREATE USER 'aur'@'localhost' IDENTIFIED BY 'aur'; + > GRANT ALL ON *.* TO 'aur'@'localhost' WITH GRANT OPTION; + > CREATE DATABASE aurweb; + > exit + +6) Prepare a database and insert dummy data: + + $ AUR_CONFIG=conf/config poetry run python -m aurweb.initdb + $ poetry run schema/gendummydata.py dummy_data.sql + $ mariadb -uaur -paur aurweb < dummy_data.sql + +7) Run the test server: + + ## set AUR_CONFIG to our locally created config + $ export AUR_CONFIG=conf/config + + ## with aurweb.spawn + $ poetry run python -m aurweb.spawn + + ## with systemd service + $ sudo install -m644 examples/aurweb.service /etc/systemd/system/ + # systemctl enable --now aurweb.service + + +Setup for running tests +----------------------- + +If you've set up a docker environment, you can run the full test-suite with: + # docker compose run test + +You can collect code-coverage data with: + $ ./util/fix-coverage data/.coverage + +See information further below on how to visualize the data. + +For running individual tests, we need to perform a couple of additional steps. +In case you did the bare-metal install, steps 2, 3, 4 and 5 should be skipped. + +1) Install the necessary packages: + + # pacman -S --needed python-poetry mariadb-libs asciidoc openssh + +2) Install the package/dependencies via `poetry`: - $ cd /path/to/aurweb/ $ poetry install 3) Copy conf/config.dev to conf/config and replace YOUR_AUR_ROOT by the absolute @@ -55,23 +135,51 @@ Bare Metal Note that when the upstream config.dev is updated, you should compare it to your conf/config, or regenerate your configuration with the command above. -4) Prepare a database: +4) Edit the config file conf/config and change the mysql/mariadb portion - $ cd /path/to/aurweb/ + We can make use of our mariadb docker container instead of having to install + mariadb. Change the config as follows: - $ AUR_CONFIG=conf/config poetry run python -m aurweb.initdb + --------------------------------------------------------------------- + ; MySQL database information. User defaults to root for containerized + ; testing with mysqldb. This should be set to a non-root user. + user = root + password = aur + host = 127.0.0.1 + port = 13306 + ;socket = /var/run/mysqld/mysqld.sock + --------------------------------------------------------------------- - $ poetry run schema/gendummydata.py dummy_data.sql - $ mysql -uaur -paur aurweb < dummy_data.sql +5) Start our mariadb docker container -5) Run the test server: + # docker compose start mariadb - ## set AUR_CONFIG to our locally created config - $ export AUR_CONFIG=conf/config +6) Set environment variables - ## with aurweb.spawn - $ poetry run python -m aurweb.spawn + $ export AUR_CONFIG=conf/config + $ export LOG_CONFIG=logging.test.conf - ## with systemd service - $ sudo install -m644 examples/aurweb.service /etc/systemd/system/ - $ systemctl enable --now aurweb.service +7) Compile translation & doc files + + $ make -C po install + $ make -C doc + +Now we can run our python test-suite or individual tests with: + + $ poetry run pytest test/ + $ poetry run pytest test/test_whatever.py + +To run Sharness tests: + + $ poetry run make -C test sh + +The e-Mails that have been generated can be found at test-emails/ + +After test runs, code-coverage reports can be created with: + ## CLI report + $ coverage report + + ## HTML version stored at htmlcov/ + $ coverage html + +More information about tests can be found at test/README.md diff --git a/aurweb/archives/__init__.py b/aurweb/archives/__init__.py new file mode 100644 index 00000000..47020641 --- /dev/null +++ b/aurweb/archives/__init__.py @@ -0,0 +1 @@ +# aurweb.archives diff --git a/aurweb/archives/spec/__init__.py b/aurweb/archives/spec/__init__.py new file mode 100644 index 00000000..b6e376b4 --- /dev/null +++ b/aurweb/archives/spec/__init__.py @@ -0,0 +1 @@ +# aurweb.archives.spec diff --git a/aurweb/archives/spec/base.py b/aurweb/archives/spec/base.py new file mode 100644 index 00000000..60f734f2 --- /dev/null +++ b/aurweb/archives/spec/base.py @@ -0,0 +1,77 @@ +from pathlib import Path +from typing import Any, Dict, Iterable, List, Set + + +class GitInfo: + """Information about a Git repository.""" + + """ Path to Git repository. """ + path: str + + """ Local Git repository configuration. """ + config: Dict[str, Any] + + def __init__(self, path: str, config: Dict[str, Any] = dict()) -> "GitInfo": + self.path = Path(path) + self.config = config + + +class SpecOutput: + """Class used for git_archive.py output details.""" + + """ Filename relative to the Git repository root. """ + filename: Path + + """ Git repository information. """ + git_info: GitInfo + + """ Bytes bound for `SpecOutput.filename`. """ + data: bytes + + def __init__(self, filename: str, git_info: GitInfo, data: bytes) -> "SpecOutput": + self.filename = filename + self.git_info = git_info + self.data = data + + +class SpecBase: + """ + Base for Spec classes defined in git_archve.py --spec modules. + + All supported --spec modules must contain the following classes: + - Spec(SpecBase) + """ + + """ A list of SpecOutputs, each of which contain output file data. """ + outputs: List[SpecOutput] = list() + + """ A set of repositories to commit changes to. """ + repos: Set[str] = set() + + def generate(self) -> Iterable[SpecOutput]: + """ + "Pure virtual" output generator. + + `SpecBase.outputs` and `SpecBase.repos` should be populated within an + overridden version of this function in SpecBase derivatives. + """ + raise NotImplementedError() + + def add_output(self, filename: str, git_info: GitInfo, data: bytes) -> None: + """ + Add a SpecOutput instance to the set of outputs. + + :param filename: Filename relative to the git repository root + :param git_info: GitInfo instance + :param data: Binary data bound for `filename` + """ + if git_info.path not in self.repos: + self.repos.add(git_info.path) + + self.outputs.append( + SpecOutput( + filename, + git_info, + data, + ) + ) diff --git a/aurweb/archives/spec/metadata.py b/aurweb/archives/spec/metadata.py new file mode 100644 index 00000000..ce7c6f30 --- /dev/null +++ b/aurweb/archives/spec/metadata.py @@ -0,0 +1,85 @@ +from typing import Iterable + +import orjson + +from aurweb import config, db +from aurweb.models import Package, PackageBase, User +from aurweb.rpc import RPC + +from .base import GitInfo, SpecBase, SpecOutput + +ORJSON_OPTS = orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2 + + +class Spec(SpecBase): + def __init__(self) -> "Spec": + self.metadata_repo = GitInfo( + config.get("git-archive", "metadata-repo"), + ) + + def generate(self) -> Iterable[SpecOutput]: + # Base query used by the RPC. + base_query = ( + db.query(Package) + .join(PackageBase) + .join(User, PackageBase.MaintainerUID == User.ID, isouter=True) + ) + + # Create an instance of RPC, use it to get entities from + # our query and perform a metadata subquery for all packages. + rpc = RPC(version=5, type="info") + print("performing package database query") + packages = rpc.entities(base_query).all() + print("performing package database subqueries") + rpc.subquery({pkg.ID for pkg in packages}) + + pkgbases, pkgnames = dict(), dict() + for package in packages: + # Produce RPC type=info data for `package` + data = rpc.get_info_json_data(package) + + pkgbase_name = data.get("PackageBase") + pkgbase_data = { + "ID": data.pop("PackageBaseID"), + "URLPath": data.pop("URLPath"), + "FirstSubmitted": data.pop("FirstSubmitted"), + "LastModified": data.pop("LastModified"), + "OutOfDate": data.pop("OutOfDate"), + "Maintainer": data.pop("Maintainer"), + "Keywords": data.pop("Keywords"), + "NumVotes": data.pop("NumVotes"), + "Popularity": data.pop("Popularity"), + "PopularityUpdated": package.PopularityUpdated.timestamp(), + } + + # Store the data in `pkgbases` dict. We do this so we only + # end up processing a single `pkgbase` if repeated after + # this loop + pkgbases[pkgbase_name] = pkgbase_data + + # Remove Popularity and NumVotes from package data. + # These fields change quite often which causes git data + # modification to explode. + # data.pop("NumVotes") + # data.pop("Popularity") + + # Remove the ID key from package json. + data.pop("ID") + + # Add the `package`.Name to the pkgnames set + name = data.get("Name") + pkgnames[name] = data + + # Add metadata outputs + self.add_output( + "pkgname.json", + self.metadata_repo, + orjson.dumps(pkgnames, option=ORJSON_OPTS), + ) + self.add_output( + "pkgbase.json", + self.metadata_repo, + orjson.dumps(pkgbases, option=ORJSON_OPTS), + ) + + return self.outputs diff --git a/aurweb/archives/spec/pkgbases.py b/aurweb/archives/spec/pkgbases.py new file mode 100644 index 00000000..db2085cc --- /dev/null +++ b/aurweb/archives/spec/pkgbases.py @@ -0,0 +1,26 @@ +from typing import Iterable + +import orjson + +from aurweb import config, db +from aurweb.models import PackageBase + +from .base import GitInfo, SpecBase, SpecOutput + +ORJSON_OPTS = orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2 + + +class Spec(SpecBase): + def __init__(self) -> "Spec": + self.pkgbases_repo = GitInfo(config.get("git-archive", "pkgbases-repo")) + + def generate(self) -> Iterable[SpecOutput]: + query = db.query(PackageBase.Name).order_by(PackageBase.Name.asc()).all() + pkgbases = [pkgbase.Name for pkgbase in query] + + self.add_output( + "pkgbase.json", + self.pkgbases_repo, + orjson.dumps(pkgbases, option=ORJSON_OPTS), + ) + return self.outputs diff --git a/aurweb/archives/spec/pkgnames.py b/aurweb/archives/spec/pkgnames.py new file mode 100644 index 00000000..3df62af6 --- /dev/null +++ b/aurweb/archives/spec/pkgnames.py @@ -0,0 +1,31 @@ +from typing import Iterable + +import orjson + +from aurweb import config, db +from aurweb.models import Package, PackageBase + +from .base import GitInfo, SpecBase, SpecOutput + +ORJSON_OPTS = orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2 + + +class Spec(SpecBase): + def __init__(self) -> "Spec": + self.pkgnames_repo = GitInfo(config.get("git-archive", "pkgnames-repo")) + + def generate(self) -> Iterable[SpecOutput]: + query = ( + db.query(Package.Name) + .join(PackageBase, PackageBase.ID == Package.PackageBaseID) + .order_by(Package.Name.asc()) + .all() + ) + pkgnames = [pkg.Name for pkg in query] + + self.add_output( + "pkgname.json", + self.pkgnames_repo, + orjson.dumps(pkgnames, option=ORJSON_OPTS), + ) + return self.outputs diff --git a/aurweb/archives/spec/users.py b/aurweb/archives/spec/users.py new file mode 100644 index 00000000..80da1641 --- /dev/null +++ b/aurweb/archives/spec/users.py @@ -0,0 +1,26 @@ +from typing import Iterable + +import orjson + +from aurweb import config, db +from aurweb.models import User + +from .base import GitInfo, SpecBase, SpecOutput + +ORJSON_OPTS = orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2 + + +class Spec(SpecBase): + def __init__(self) -> "Spec": + self.users_repo = GitInfo(config.get("git-archive", "users-repo")) + + def generate(self) -> Iterable[SpecOutput]: + query = db.query(User.Username).order_by(User.Username.asc()).all() + users = [user.Username for user in query] + + self.add_output( + "users.json", + self.users_repo, + orjson.dumps(users, option=ORJSON_OPTS), + ) + return self.outputs diff --git a/aurweb/asgi.py b/aurweb/asgi.py index fa2526ed..4a0c1113 100644 --- a/aurweb/asgi.py +++ b/aurweb/asgi.py @@ -6,17 +6,21 @@ import re import sys import traceback import typing - +from contextlib import asynccontextmanager from urllib.parse import quote_plus import requests - from fastapi import FastAPI, HTTPException, Request, Response from fastapi.responses import RedirectResponse from fastapi.staticfiles import StaticFiles from jinja2 import TemplateNotFound -from prometheus_client import multiprocess -from sqlalchemy import and_, or_ +from opentelemetry import trace +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor +from sqlalchemy import and_ from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.middleware.authentication import AuthenticationMiddleware from starlette.middleware.sessions import SessionMiddleware @@ -24,23 +28,29 @@ from starlette.middleware.sessions import SessionMiddleware import aurweb.captcha # noqa: F401 import aurweb.config import aurweb.filters # noqa: F401 -import aurweb.logging -import aurweb.pkgbase.util as pkgbaseutil - -from aurweb import logging, prometheus, util +from aurweb import aur_logging, prometheus, util +from aurweb.aur_redis import redis_connection from aurweb.auth import BasicAuthBackend from aurweb.db import get_engine, query from aurweb.models import AcceptedTerm, Term from aurweb.packages.util import get_pkg_or_base from aurweb.prometheus import instrumentator -from aurweb.redis import redis_connection from aurweb.routers import APP_ROUTES from aurweb.templates import make_context, render_template -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) +session_secret = aurweb.config.get("fastapi", "session_secret") + + +@asynccontextmanager +async def lifespan(app: FastAPI): + await app_startup() + yield + # Setup the FastAPI app. -app = FastAPI() +app = FastAPI(lifespan=lifespan) + # Instrument routes with the prometheus-fastapi-instrumentator # library with custom collectors and expose /metrics. @@ -49,7 +59,17 @@ instrumentator().add(prometheus.http_requests_total()) instrumentator().instrument(app) -@app.on_event("startup") +# Instrument FastAPI for tracing +FastAPIInstrumentor.instrument_app(app) + +resource = Resource(attributes={"service.name": "aurweb"}) +otlp_endpoint = aurweb.config.get("tracing", "otlp_endpoint") +otlp_exporter = OTLPSpanExporter(endpoint=otlp_endpoint) +span_processor = BatchSpanProcessor(otlp_exporter) +trace.set_tracer_provider(TracerProvider(resource=resource)) +trace.get_tracer_provider().add_span_processor(span_processor) + + async def app_startup(): # https://stackoverflow.com/questions/67054759/about-the-maximum-recursion-error-in-fastapi # Test failures have been observed by internal starlette code when @@ -60,53 +80,39 @@ async def app_startup(): # provided by the user. Docker uses .env's TEST_RECURSION_LIMIT # when running test suites. # TODO: Find a proper fix to this issue. - recursion_limit = int(os.environ.get( - "TEST_RECURSION_LIMIT", sys.getrecursionlimit() + 1000)) + recursion_limit = int( + os.environ.get("TEST_RECURSION_LIMIT", sys.getrecursionlimit() + 1000) + ) sys.setrecursionlimit(recursion_limit) backend = aurweb.config.get("database", "backend") if backend not in aurweb.db.DRIVERS: raise ValueError( f"The configured database backend ({backend}) is unsupported. " - f"Supported backends: {str(aurweb.db.DRIVERS.keys())}") + f"Supported backends: {str(aurweb.db.DRIVERS.keys())}" + ) - session_secret = aurweb.config.get("fastapi", "session_secret") if not session_secret: raise Exception("[fastapi] session_secret must not be empty") if not os.environ.get("PROMETHEUS_MULTIPROC_DIR", None): - logger.warning("$PROMETHEUS_MULTIPROC_DIR is not set, the /metrics " - "endpoint is disabled.") + logger.warning( + "$PROMETHEUS_MULTIPROC_DIR is not set, the /metrics " + "endpoint is disabled." + ) - app.mount("/static/css", - StaticFiles(directory="web/html/css"), - name="static_css") - app.mount("/static/js", - StaticFiles(directory="web/html/js"), - name="static_js") - app.mount("/static/images", - StaticFiles(directory="web/html/images"), - name="static_images") - - # Add application middlewares. - app.add_middleware(AuthenticationMiddleware, backend=BasicAuthBackend()) - app.add_middleware(SessionMiddleware, secret_key=session_secret) + app.mount("/static", StaticFiles(directory="static"), name="static_files") # Add application routes. def add_router(module): app.include_router(module.router) + util.apply_all(APP_ROUTES, add_router) # Initialize the database engine and ORM. get_engine() -def child_exit(server, worker): # pragma: no cover - """ This function is required for gunicorn customization - of prometheus multiprocessing. """ - multiprocess.mark_process_dead(worker.pid) - - async def internal_server_error(request: Request, exc: Exception) -> Response: """ Catch all uncaught Exceptions thrown in a route. @@ -177,9 +183,7 @@ async def internal_server_error(request: Request, exc: Exception) -> Response: else: # post form_data = str(dict(request.state.form_data)) - desc = desc + [ - f"- Data: `{form_data}`" - ] + ["", f"```{tb}```"] + desc = desc + [f"- Data: `{form_data}`"] + ["", f"```{tb}```"] headers = {"Authorization": f"Bearer {token}"} data = { @@ -191,11 +195,12 @@ async def internal_server_error(request: Request, exc: Exception) -> Response: logger.info(endp) resp = requests.post(endp, json=data, headers=headers) if resp.status_code != http.HTTPStatus.CREATED: - logger.error( - f"Unable to report exception to {repo}: {resp.text}") + logger.error(f"Unable to report exception to {repo}: {resp.text}") else: - logger.warning("Unable to report an exception found due to " - "unset notifications.error-{{project,token}}") + logger.warning( + "Unable to report an exception found due to " + "unset notifications.error-{{project,token}}" + ) # Log details about the exception traceback. logger.error(f"FATAL[{tb_id}]: An unexpected exception has occurred.") @@ -203,14 +208,17 @@ async def internal_server_error(request: Request, exc: Exception) -> Response: else: retval = retval.decode() - return render_template(request, "errors/500.html", context, - status_code=http.HTTPStatus.INTERNAL_SERVER_ERROR) + return render_template( + request, + "errors/500.html", + context, + status_code=http.HTTPStatus.INTERNAL_SERVER_ERROR, + ) @app.exception_handler(StarletteHTTPException) -async def http_exception_handler(request: Request, exc: HTTPException) \ - -> Response: - """ Handle an HTTPException thrown in a route. """ +async def http_exception_handler(request: Request, exc: HTTPException) -> Response: + """Handle an HTTPException thrown in a route.""" phrase = http.HTTPStatus(exc.status_code).phrase context = make_context(request, phrase) context["exc"] = exc @@ -220,24 +228,30 @@ async def http_exception_handler(request: Request, exc: HTTPException) \ if exc.status_code == http.HTTPStatus.NOT_FOUND: tokens = request.url.path.split("/") matches = re.match("^([a-z0-9][a-z0-9.+_-]*?)(\\.git)?$", tokens[1]) - if matches: + if matches and len(tokens) == 2: try: pkgbase = get_pkg_or_base(matches.group(1)) - context = pkgbaseutil.make_context(request, pkgbase) + context["pkgbase"] = pkgbase + context["git_clone_uri_anon"] = aurweb.config.get( + "options", "git_clone_uri_anon" + ) + context["git_clone_uri_priv"] = aurweb.config.get( + "options", "git_clone_uri_priv" + ) except HTTPException: pass try: - return render_template(request, f"errors/{exc.status_code}.html", - context, exc.status_code) + return render_template( + request, f"errors/{exc.status_code}.html", context, exc.status_code + ) except TemplateNotFound: - return render_template(request, "errors/detail.html", - context, exc.status_code) + return render_template(request, "errors/detail.html", context, exc.status_code) @app.middleware("http") async def add_security_headers(request: Request, call_next: typing.Callable): - """ This middleware adds the CSP, XCTO, XFO and RP security + """This middleware adds the CSP, XCTO, XFO and RP security headers to the HTTP response associated with request. CSP: Content-Security-Policy @@ -253,10 +267,16 @@ async def add_security_headers(request: Request, call_next: typing.Callable): # Add CSP header. nonce = request.user.nonce csp = "default-src 'self'; " - script_hosts = [] - csp += f"script-src 'self' 'nonce-{nonce}' " + ' '.join(script_hosts) - # It's fine if css is inlined. - csp += "; style-src 'self' 'unsafe-inline'" + + # swagger-ui needs access to cdn.jsdelivr.net javascript + script_hosts = ["cdn.jsdelivr.net"] + csp += f"script-src 'self' 'unsafe-inline' 'nonce-{nonce}' " + " ".join( + script_hosts + ) + + # swagger-ui needs access to cdn.jsdelivr.net css + css_hosts = ["cdn.jsdelivr.net"] + csp += "; style-src 'self' 'unsafe-inline' " + " ".join(css_hosts) response.headers["Content-Security-Policy"] = csp # Add XTCO header. @@ -276,17 +296,22 @@ async def add_security_headers(request: Request, call_next: typing.Callable): @app.middleware("http") async def check_terms_of_service(request: Request, call_next: typing.Callable): - """ This middleware function redirects authenticated users if they - have any outstanding Terms to agree to. """ + """This middleware function redirects authenticated users if they + have any outstanding Terms to agree to.""" if request.user.is_authenticated() and request.url.path != "/tos": - unaccepted = query(Term).join(AcceptedTerm).filter( - or_(AcceptedTerm.UsersID != request.user.ID, - and_(AcceptedTerm.UsersID == request.user.ID, - AcceptedTerm.TermsID == Term.ID, - AcceptedTerm.Revision < Term.Revision))) - if query(Term).count() > unaccepted.count(): - return RedirectResponse( - "/tos", status_code=int(http.HTTPStatus.SEE_OTHER)) + accepted = ( + query(Term) + .join(AcceptedTerm) + .filter( + and_( + AcceptedTerm.UsersID == request.user.ID, + AcceptedTerm.TermsID == Term.ID, + AcceptedTerm.Revision >= Term.Revision, + ), + ) + ) + if query(Term).count() - accepted.count() > 0: + return RedirectResponse("/tos", status_code=int(http.HTTPStatus.SEE_OTHER)) return await util.error_or_result(call_next, request) @@ -301,9 +326,14 @@ async def id_redirect_middleware(request: Request, call_next: typing.Callable): for k, v in request.query_params.items(): if k != "id": qs.append(f"{k}={quote_plus(str(v))}") - qs = str() if not qs else '?' + '&'.join(qs) + qs = str() if not qs else "?" + "&".join(qs) - path = request.url.path.rstrip('/') + path = request.url.path.rstrip("/") return RedirectResponse(f"{path}/{id}{qs}") return await util.error_or_result(call_next, request) + + +# Add application middlewares. +app.add_middleware(AuthenticationMiddleware, backend=BasicAuthBackend()) +app.add_middleware(SessionMiddleware, secret_key=session_secret) diff --git a/aurweb/logging.py b/aurweb/aur_logging.py similarity index 92% rename from aurweb/logging.py rename to aurweb/aur_logging.py index 116421e4..d90edfdd 100644 --- a/aurweb/logging.py +++ b/aurweb/aur_logging.py @@ -15,7 +15,7 @@ logging.getLogger("root").addHandler(logging.NullHandler()) def get_logger(name: str) -> logging.Logger: - """ A logging.getLogger wrapper. Importing this function and + """A logging.getLogger wrapper. Importing this function and using it to get a module-local logger ensures that logging.conf initialization is performed wherever loggers are used. diff --git a/aurweb/redis.py b/aurweb/aur_redis.py similarity index 85% rename from aurweb/redis.py rename to aurweb/aur_redis.py index e29b8e37..b735bb84 100644 --- a/aurweb/redis.py +++ b/aurweb/aur_redis.py @@ -1,17 +1,18 @@ import fakeredis - +from opentelemetry.instrumentation.redis import RedisInstrumentor from redis import ConnectionPool, Redis import aurweb.config +from aurweb import aur_logging -from aurweb import logging - -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) pool = None +RedisInstrumentor().instrument() + class FakeConnectionPool: - """ A fake ConnectionPool class which holds an internal reference + """A fake ConnectionPool class which holds an internal reference to a fakeredis handle. We normally deal with Redis by keeping its ConnectionPool globally diff --git a/aurweb/auth/__init__.py b/aurweb/auth/__init__.py index cb6f3e4d..e895dcdb 100644 --- a/aurweb/auth/__init__.py +++ b/aurweb/auth/__init__.py @@ -1,25 +1,22 @@ import functools - from http import HTTPStatus from typing import Callable import fastapi - from fastapi import HTTPException from fastapi.responses import RedirectResponse from starlette.authentication import AuthCredentials, AuthenticationBackend from starlette.requests import HTTPConnection import aurweb.config - from aurweb import db, filters, l10n, time, util from aurweb.models import Session, User from aurweb.models.account_type import ACCOUNT_TYPE_ID class StubQuery: - """ Acts as a stubbed version of an orm.Query. Typically used - to masquerade fake records for an AnonymousUser. """ + """Acts as a stubbed version of an orm.Query. Typically used + to masquerade fake records for an AnonymousUser.""" def filter(self, *args): return StubQuery() @@ -29,19 +26,21 @@ class StubQuery: class AnonymousUser: - """ A stubbed User class used when an unauthenticated User - makes a request against FastAPI. """ + """A stubbed User class used when an unauthenticated User + makes a request against FastAPI.""" + # Stub attributes used to mimic a real user. ID = 0 Username = "N/A" Email = "N/A" class AccountType: - """ A stubbed AccountType static class. In here, we use an ID + """A stubbed AccountType static class. In here, we use an ID and AccountType which do not exist in our constant records. All records primary keys (AccountType.ID) should be non-zero, so using a zero here means that we'll never match against a - real AccountType. """ + real AccountType.""" + ID = 0 AccountType = "Anonymous" @@ -72,7 +71,7 @@ class AnonymousUser: return False @staticmethod - def is_trusted_user(): + def is_package_maintainer(): return False @staticmethod @@ -97,6 +96,7 @@ class AnonymousUser: class BasicAuthBackend(AuthenticationBackend): + @db.async_retry_deadlock async def authenticate(self, conn: HTTPConnection): unauthenticated = (None, AnonymousUser()) sid = conn.cookies.get("AURSID") @@ -104,11 +104,9 @@ class BasicAuthBackend(AuthenticationBackend): return unauthenticated timeout = aurweb.config.getint("options", "login_timeout") - remembered = ("AURREMEMBER" in conn.cookies - and bool(conn.cookies.get("AURREMEMBER"))) + remembered = conn.cookies.get("AURREMEMBER") == "True" if remembered: - timeout = aurweb.config.getint("options", - "persistent_cookie_timeout") + timeout = aurweb.config.getint("options", "persistent_cookie_timeout") # If no session with sid and a LastUpdateTS now or later exists. now_ts = time.utcnow() @@ -123,12 +121,11 @@ class BasicAuthBackend(AuthenticationBackend): # At this point, we cannot have an invalid user if the record # exists, due to ForeignKey constraints in the schema upheld # by mysqlclient. - with db.begin(): - user = db.query(User).filter(User.ID == record.UsersID).first() + user = db.query(User).filter(User.ID == record.UsersID).first() user.nonce = util.make_nonce() user.authenticated = True - return (AuthCredentials(["authenticated"]), user) + return AuthCredentials(["authenticated"]), user def _auth_required(auth_goal: bool = True): @@ -160,40 +157,45 @@ def _auth_required(auth_goal: bool = True): # page itself is not directly possible (e.g. submitting a form). if request.method in ("GET", "HEAD"): url = request.url.path - elif (referer := request.headers.get("Referer")): + elif referer := request.headers.get("Referer"): aur = aurweb.config.get("options", "aur_location") + "/" if not referer.startswith(aur): _ = l10n.get_translator_for_request(request) - raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, - detail=_("Bad Referer header.")) - url = referer[len(aur) - 1:] + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=_("Bad Referer header."), + ) + url = referer[len(aur) - 1 :] url = "/login?" + filters.urlencode({"next": url}) return RedirectResponse(url, status_code=int(HTTPStatus.SEE_OTHER)) + return wrapper return decorator def requires_auth(func: Callable) -> Callable: - """ Require an authenticated session for a particular route. """ + """Require an authenticated session for a particular route.""" @functools.wraps(func) async def wrapper(*args, **kwargs): return await _auth_required(True)(func)(*args, **kwargs) + return wrapper def requires_guest(func: Callable) -> Callable: - """ Require a guest (unauthenticated) session for a particular route. """ + """Require a guest (unauthenticated) session for a particular route.""" @functools.wraps(func) async def wrapper(*args, **kwargs): return await _auth_required(False)(func)(*args, **kwargs) + return wrapper def account_type_required(one_of: set): - """ A decorator that can be used on FastAPI routes to dictate + """A decorator that can be used on FastAPI routes to dictate that a user belongs to one of the types defined in one_of. This decorator should be run after an @auth_required(True) is @@ -203,7 +205,7 @@ def account_type_required(one_of: set): @router.get('/some_route') @auth_required(True) - @account_type_required({"Trusted User", "Trusted User & Developer"}) + @account_type_required({"Package Maintainer", "Package Maintainer & Developer"}) async def some_route(request: fastapi.Request): return Response() @@ -211,18 +213,15 @@ def account_type_required(one_of: set): :return: Return the FastAPI function this decorator wraps. """ # Convert any account type string constants to their integer IDs. - one_of = { - ACCOUNT_TYPE_ID[atype] - for atype in one_of - if isinstance(atype, str) - } + one_of = {ACCOUNT_TYPE_ID[atype] for atype in one_of if isinstance(atype, str)} def decorator(func): @functools.wraps(func) async def wrapper(request: fastapi.Request, *args, **kwargs): if request.user.AccountTypeID not in one_of: - return RedirectResponse("/", - status_code=int(HTTPStatus.SEE_OTHER)) + return RedirectResponse("/", status_code=int(HTTPStatus.SEE_OTHER)) return await func(request, *args, **kwargs) + return wrapper + return decorator diff --git a/aurweb/auth/creds.py b/aurweb/auth/creds.py index 100aad8c..594188ca 100644 --- a/aurweb/auth/creds.py +++ b/aurweb/auth/creds.py @@ -1,4 +1,9 @@ -from aurweb.models.account_type import DEVELOPER_ID, TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID, USER_ID +from aurweb.models.account_type import ( + DEVELOPER_ID, + PACKAGE_MAINTAINER_AND_DEV_ID, + PACKAGE_MAINTAINER_ID, + USER_ID, +) from aurweb.models.user import User ACCOUNT_CHANGE_TYPE = 1 @@ -25,52 +30,53 @@ PKGBASE_VOTE = 16 PKGREQ_FILE = 23 PKGREQ_CLOSE = 17 PKGREQ_LIST = 18 -TU_ADD_VOTE = 19 -TU_LIST_VOTES = 20 -TU_VOTE = 21 +PM_ADD_VOTE = 19 +PM_LIST_VOTES = 20 +PM_VOTE = 21 PKGBASE_MERGE = 29 -user_developer_or_trusted_user = set([USER_ID, TRUSTED_USER_ID, DEVELOPER_ID, TRUSTED_USER_AND_DEV_ID]) -trusted_user_or_dev = set([TRUSTED_USER_ID, DEVELOPER_ID, TRUSTED_USER_AND_DEV_ID]) -developer = set([DEVELOPER_ID, TRUSTED_USER_AND_DEV_ID]) -trusted_user = set([TRUSTED_USER_ID, TRUSTED_USER_AND_DEV_ID]) +user_developer_or_package_maintainer = set( + [USER_ID, PACKAGE_MAINTAINER_ID, DEVELOPER_ID, PACKAGE_MAINTAINER_AND_DEV_ID] +) +package_maintainer_or_dev = set( + [PACKAGE_MAINTAINER_ID, DEVELOPER_ID, PACKAGE_MAINTAINER_AND_DEV_ID] +) +developer = set([DEVELOPER_ID, PACKAGE_MAINTAINER_AND_DEV_ID]) +package_maintainer = set([PACKAGE_MAINTAINER_ID, PACKAGE_MAINTAINER_AND_DEV_ID]) cred_filters = { - PKGBASE_FLAG: user_developer_or_trusted_user, - PKGBASE_NOTIFY: user_developer_or_trusted_user, - PKGBASE_VOTE: user_developer_or_trusted_user, - PKGREQ_FILE: user_developer_or_trusted_user, - ACCOUNT_CHANGE_TYPE: trusted_user_or_dev, - ACCOUNT_EDIT: trusted_user_or_dev, - ACCOUNT_LAST_LOGIN: trusted_user_or_dev, - ACCOUNT_LIST_COMMENTS: trusted_user_or_dev, - ACCOUNT_SEARCH: trusted_user_or_dev, - COMMENT_DELETE: trusted_user_or_dev, - COMMENT_UNDELETE: trusted_user_or_dev, - COMMENT_VIEW_DELETED: trusted_user_or_dev, - COMMENT_EDIT: trusted_user_or_dev, - COMMENT_PIN: trusted_user_or_dev, - PKGBASE_ADOPT: trusted_user_or_dev, - PKGBASE_SET_KEYWORDS: trusted_user_or_dev, - PKGBASE_DELETE: trusted_user_or_dev, - PKGBASE_EDIT_COMAINTAINERS: trusted_user_or_dev, - PKGBASE_DISOWN: trusted_user_or_dev, - PKGBASE_LIST_VOTERS: trusted_user_or_dev, - PKGBASE_UNFLAG: trusted_user_or_dev, - PKGREQ_CLOSE: trusted_user_or_dev, - PKGREQ_LIST: trusted_user_or_dev, - TU_ADD_VOTE: trusted_user, - TU_LIST_VOTES: trusted_user_or_dev, - TU_VOTE: trusted_user, + PKGBASE_FLAG: user_developer_or_package_maintainer, + PKGBASE_NOTIFY: user_developer_or_package_maintainer, + PKGBASE_VOTE: user_developer_or_package_maintainer, + PKGREQ_FILE: user_developer_or_package_maintainer, + ACCOUNT_CHANGE_TYPE: package_maintainer_or_dev, + ACCOUNT_EDIT: package_maintainer_or_dev, + ACCOUNT_LAST_LOGIN: package_maintainer_or_dev, + ACCOUNT_LIST_COMMENTS: package_maintainer_or_dev, + ACCOUNT_SEARCH: package_maintainer_or_dev, + COMMENT_DELETE: package_maintainer_or_dev, + COMMENT_UNDELETE: package_maintainer_or_dev, + COMMENT_VIEW_DELETED: package_maintainer_or_dev, + COMMENT_EDIT: package_maintainer_or_dev, + COMMENT_PIN: package_maintainer_or_dev, + PKGBASE_ADOPT: package_maintainer_or_dev, + PKGBASE_SET_KEYWORDS: package_maintainer_or_dev, + PKGBASE_DELETE: package_maintainer_or_dev, + PKGBASE_EDIT_COMAINTAINERS: package_maintainer_or_dev, + PKGBASE_DISOWN: package_maintainer_or_dev, + PKGBASE_LIST_VOTERS: package_maintainer_or_dev, + PKGBASE_UNFLAG: package_maintainer_or_dev, + PKGREQ_CLOSE: package_maintainer_or_dev, + PKGREQ_LIST: package_maintainer_or_dev, + PM_ADD_VOTE: package_maintainer, + PM_LIST_VOTES: package_maintainer_or_dev, + PM_VOTE: package_maintainer, ACCOUNT_EDIT_DEV: developer, - PKGBASE_MERGE: trusted_user_or_dev, + PKGBASE_MERGE: package_maintainer_or_dev, } -def has_credential(user: User, - credential: int, - approved_users: list = tuple()): - - if user in approved_users: +def has_credential(user: User, credential: int, approved: list = tuple()): + if user in approved: return True return user.AccountTypeID in cred_filters[credential] diff --git a/aurweb/benchmark.py b/aurweb/benchmark.py index 7086fb08..83969962 100644 --- a/aurweb/benchmark.py +++ b/aurweb/benchmark.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import UTC, datetime class Benchmark: @@ -6,16 +6,16 @@ class Benchmark: self.start() def _timestamp(self) -> float: - """ Generate a timestamp. """ - return float(datetime.utcnow().timestamp()) + """Generate a timestamp.""" + return float(datetime.now(UTC).timestamp()) def start(self) -> int: - """ Start a benchmark. """ + """Start a benchmark.""" self.current = self._timestamp() return self.current def end(self): - """ Return the diff between now - start(). """ + """Return the diff between now - start().""" n = self._timestamp() - self.current self.current = float(0) return n diff --git a/aurweb/cache.py b/aurweb/cache.py index 697473b8..477f6780 100644 --- a/aurweb/cache.py +++ b/aurweb/cache.py @@ -1,20 +1,64 @@ -from redis import Redis +import pickle +from typing import Any, Callable + from sqlalchemy import orm +from aurweb import config +from aurweb.aur_redis import redis_connection +from aurweb.prometheus import SEARCH_REQUESTS -async def db_count_cache(redis: Redis, key: str, query: orm.Query, - expire: int = None) -> int: - """ Store and retrieve a query.count() via redis cache. +_redis = redis_connection() + + +def lambda_cache(key: str, value: Callable[[], Any], expire: int = None) -> list: + """Store and retrieve lambda results via redis cache. + + :param key: Redis key + :param value: Lambda callable returning the value + :param expire: Optional expiration in seconds + :return: result of callable or cache + """ + result = _redis.get(key) + if result is not None: + return pickle.loads(result) + + _redis.set(key, (pickle.dumps(result := value())), ex=expire) + return result + + +def db_count_cache(key: str, query: orm.Query, expire: int = None) -> int: + """Store and retrieve a query.count() via redis cache. - :param redis: Redis handle :param key: Redis key :param query: SQLAlchemy ORM query :param expire: Optional expiration in seconds :return: query.count() """ - result = redis.get(key) + result = _redis.get(key) if result is None: - redis.set(key, (result := int(query.count()))) + _redis.set(key, (result := int(query.count()))) if expire: - redis.expire(key, expire) + _redis.expire(key, expire) return int(result) + + +def db_query_cache(key: str, query: orm.Query, expire: int = None) -> list: + """Store and retrieve query results via redis cache. + + :param key: Redis key + :param query: SQLAlchemy ORM query + :param expire: Optional expiration in seconds + :return: query.all() + """ + result = _redis.get(key) + if result is None: + SEARCH_REQUESTS.labels(cache="miss").inc() + if _redis.dbsize() > config.getint("cache", "max_search_entries", 50000): + return query.all() + _redis.set(key, (result := pickle.dumps(query.all()))) + if expire: + _redis.expire(key, expire) + else: + SEARCH_REQUESTS.labels(cache="hit").inc() + + return pickle.loads(result) diff --git a/aurweb/captcha.py b/aurweb/captcha.py index 34d99e53..52e834ac 100644 --- a/aurweb/captcha.py +++ b/aurweb/captcha.py @@ -1,7 +1,9 @@ """ This module consists of aurweb's CAPTCHA utility functions and filters. """ + import hashlib from jinja2 import pass_context +from sqlalchemy import func from aurweb.db import query from aurweb.models import User @@ -9,8 +11,9 @@ from aurweb.templates import register_filter def get_captcha_salts(): - """ Produce salts based on the current user count. """ - count = query(User).count() + """Produce salts based on the current user count.""" + count = query(func.count(User.ID)).scalar() + salts = [] for i in range(0, 6): salts.append(f"aurweb-{count - i}") @@ -18,19 +21,19 @@ def get_captcha_salts(): def get_captcha_token(salt): - """ Produce a token for the CAPTCHA salt. """ + """Produce a token for the CAPTCHA salt.""" return hashlib.md5(salt.encode()).hexdigest()[:3] def get_captcha_challenge(salt): - """ Get a CAPTCHA challenge string (shell command) for a salt. """ + """Get a CAPTCHA challenge string (shell command) for a salt.""" token = get_captcha_token(salt) return f"LC_ALL=C pacman -V|sed -r 's#[0-9]+#{token}#g'|md5sum|cut -c1-6" def get_captcha_answer(token): - """ Compute the answer via md5 of the real template text, return the - first six digits of the hexadecimal hash. """ + """Compute the answer via md5 of the real template text, return the + first six digits of the hexadecimal hash.""" text = r""" .--. Pacman v%s.%s.%s - libalpm v%s.%s.%s / _.-' .-. .-. .-. Copyright (C) %s-%s Pacman Development Team @@ -38,14 +41,16 @@ def get_captcha_answer(token): '--' This program may be freely redistributed under the terms of the GNU General Public License. -""" % tuple([token] * 10) +""" % tuple( + [token] * 10 + ) return hashlib.md5((text + "\n").encode()).hexdigest()[:6] @register_filter("captcha_salt") @pass_context def captcha_salt_filter(context): - """ Returns the most recent CAPTCHA salt in the list of salts. """ + """Returns the most recent CAPTCHA salt in the list of salts.""" salts = get_captcha_salts() return salts[0] @@ -53,5 +58,5 @@ def captcha_salt_filter(context): @register_filter("captcha_cmdline") @pass_context def captcha_cmdline_filter(context, salt): - """ Returns a CAPTCHA challenge for a given salt. """ + """Returns a CAPTCHA challenge for a given salt.""" return get_captcha_challenge(salt) diff --git a/aurweb/config.py b/aurweb/config.py index 287152d4..46c6b182 100644 --- a/aurweb/config.py +++ b/aurweb/config.py @@ -1,12 +1,8 @@ import configparser import os - from typing import Any -# Publicly visible version of aurweb. This is used to display -# aurweb versioning in the footer and must be maintained. -# Todo: Make this dynamic/automated. -AURWEB_VERSION = "v6.0.24" +import tomlkit _parser = None @@ -15,8 +11,8 @@ def _get_parser(): global _parser if not _parser: - path = os.environ.get('AUR_CONFIG', '/etc/aurweb/config') - defaults = os.environ.get('AUR_CONFIG_DEFAULTS', path + '.defaults') + path = os.environ.get("AUR_CONFIG", "/etc/aurweb/config") + defaults = os.environ.get("AUR_CONFIG_DEFAULTS", path + ".defaults") _parser = configparser.RawConfigParser() _parser.optionxform = lambda option: option @@ -29,7 +25,7 @@ def _get_parser(): def rehash(): - """ Globally rehash the configuration parser. """ + """Globally rehash the configuration parser.""" global _parser _parser = None _get_parser() @@ -43,6 +39,18 @@ def get(section, option): return _get_parser().get(section, option) +def _get_project_meta(): + with open(os.path.join(get("options", "aurwebdir"), "pyproject.toml")) as pyproject: + file_contents = pyproject.read() + + return tomlkit.parse(file_contents)["tool"]["poetry"] + + +# Publicly visible version of aurweb. This is used to display +# aurweb versioning in the footer and must be maintained. +AURWEB_VERSION = str(_get_project_meta()["version"]) + + def getboolean(section, option): return _get_parser().getboolean(section, option) diff --git a/aurweb/cookies.py b/aurweb/cookies.py index 442a4c0a..84c43f9b 100644 --- a/aurweb/cookies.py +++ b/aurweb/cookies.py @@ -1,68 +1,8 @@ -from fastapi import Request -from fastapi.responses import Response - -from aurweb import config - - def samesite() -> str: - """ Produce cookie SameSite value based on options.disable_http_login. + """Produce cookie SameSite value. - When options.disable_http_login is True, "strict" is returned. Otherwise, - "lax" is returned. + Currently this is hard-coded to return "lax" - :returns "strict" if options.disable_http_login else "lax" + :returns "lax" """ - secure = config.getboolean("options", "disable_http_login") - return "strict" if secure else "lax" - - -def timeout(extended: bool) -> int: - """ Produce a session timeout based on `remember_me`. - - This method returns one of AUR_CONFIG's options.persistent_cookie_timeout - and options.login_timeout based on the `extended` argument. - - The `extended` argument is typically the value of the AURREMEMBER - cookie, defaulted to False. - - If `extended` is False, options.login_timeout is returned. Otherwise, - if `extended` is True, options.persistent_cookie_timeout is returned. - - :param extended: Flag which generates an extended timeout when True - :returns: Cookie timeout based on configuration options - """ - timeout = config.getint("options", "login_timeout") - if bool(extended): - timeout = config.getint("options", "persistent_cookie_timeout") - return timeout - - -def update_response_cookies(request: Request, response: Response, - aurtz: str = None, aurlang: str = None, - aursid: str = None) -> Response: - """ Update session cookies. This method is particularly useful - when updating a cookie which was already set. - - The AURSID cookie's expiration is based on the AURREMEMBER cookie, - which is retrieved from `request`. - - :param request: FastAPI request - :param response: FastAPI response - :param aurtz: Optional AURTZ cookie value - :param aurlang: Optional AURLANG cookie value - :param aursid: Optional AURSID cookie value - :returns: Updated response - """ - secure = config.getboolean("options", "disable_http_login") - if aurtz: - response.set_cookie("AURTZ", aurtz, secure=secure, httponly=secure, - samesite=samesite()) - if aurlang: - response.set_cookie("AURLANG", aurlang, secure=secure, httponly=secure, - samesite=samesite()) - if aursid: - remember_me = bool(request.cookies.get("AURREMEMBER", False)) - response.set_cookie("AURSID", aursid, secure=secure, httponly=secure, - max_age=timeout(remember_me), - samesite=samesite()) - return response + return "lax" diff --git a/aurweb/db.py b/aurweb/db.py index 4c53730a..f1b8210f 100644 --- a/aurweb/db.py +++ b/aurweb/db.py @@ -1,34 +1,14 @@ -import functools -import hashlib -import math -import os -import re - -from typing import Iterable, NewType - -import sqlalchemy - -from sqlalchemy import create_engine, event -from sqlalchemy.engine.base import Engine -from sqlalchemy.engine.url import URL -from sqlalchemy.orm import Query, Session, SessionTransaction, scoped_session, sessionmaker - -import aurweb.config -import aurweb.util - -DRIVERS = { - "mysql": "mysql+mysqldb" -} - -# Some types we don't get access to in this module. -Base = NewType("Base", "aurweb.models.declarative_base.Base") +# Supported database drivers. +DRIVERS = {"mysql": "mysql+mysqldb"} def make_random_value(table: str, column: str, length: int): - """ Generate a unique, random value for a string column in a table. + """Generate a unique, random value for a string column in a table. :return: A unique string that is not in the database """ + import aurweb.util + string = aurweb.util.make_random_string(length) while query(table).filter(column == string).first(): string = aurweb.util.make_random_string(length) @@ -52,8 +32,11 @@ def test_name() -> str: :return: Unhashed database name """ - db = os.environ.get("PYTEST_CURRENT_TEST", - aurweb.config.get("database", "name")) + import os + + import aurweb.config + + db = os.environ.get("PYTEST_CURRENT_TEST", aurweb.config.get("database", "name")) return db.split(":")[0] @@ -70,7 +53,11 @@ def name() -> str: dbname = test_name() if not dbname.startswith("test/"): return dbname + + import hashlib + sha1 = hashlib.sha1(dbname.encode()).hexdigest() + return "db" + sha1 @@ -78,18 +65,20 @@ def name() -> str: _sessions = dict() -def get_session(engine: Engine = None) -> Session: - """ Return aurweb.db's global session. """ +def get_session(engine=None): + """Return aurweb.db's global session.""" dbname = name() global _sessions if dbname not in _sessions: + from sqlalchemy.orm import scoped_session, sessionmaker if not engine: # pragma: no cover engine = get_engine() Session = scoped_session( - sessionmaker(autocommit=True, autoflush=False, bind=engine)) + sessionmaker(autocommit=True, autoflush=False, bind=engine) + ) _sessions[dbname] = Session() return _sessions.get(dbname) @@ -106,13 +95,17 @@ def pop_session(dbname: str) -> None: _sessions.pop(dbname) -def refresh(model: Base) -> Base: - """ Refresh the session's knowledge of `model`. """ +def refresh(model): + """ + Refresh the session's knowledge of `model`. + + :returns: Passed in `model` + """ get_session().refresh(model) return model -def query(Model: Base, *args, **kwargs) -> Query: +def query(Model, *args, **kwargs): """ Perform an ORM query against the database session. @@ -124,7 +117,7 @@ def query(Model: Base, *args, **kwargs) -> Query: return get_session().query(Model).filter(*args, **kwargs) -def create(Model: Base, *args, **kwargs) -> Base: +def create(Model, *args, **kwargs): """ Create a record and add() it to the database session. @@ -135,7 +128,7 @@ def create(Model: Base, *args, **kwargs) -> Base: return add(instance) -def delete(model: Base) -> None: +def delete(model) -> None: """ Delete a set of records found by Query.filter(*args, **kwargs). @@ -144,81 +137,133 @@ def delete(model: Base) -> None: get_session().delete(model) -def delete_all(iterable: Iterable) -> None: - """ Delete each instance found in `iterable`. """ +def delete_all(iterable) -> None: + """Delete each instance found in `iterable`.""" + import aurweb.util + session_ = get_session() aurweb.util.apply_all(iterable, session_.delete) def rollback() -> None: - """ Rollback the database session. """ + """Rollback the database session.""" get_session().rollback() -def add(model: Base) -> Base: - """ Add `model` to the database session. """ +def add(model): + """Add `model` to the database session.""" get_session().add(model) return model -def begin() -> SessionTransaction: - """ Begin an SQLAlchemy SessionTransaction. """ +def begin(): + """Begin an SQLAlchemy SessionTransaction.""" return get_session().begin() -def get_sqlalchemy_url() -> URL: +def retry_deadlock(func): + from sqlalchemy.exc import OperationalError + + def wrapper(*args, _i: int = 0, **kwargs): + # Retry 10 times, then raise the exception + # If we fail before the 10th, recurse into `wrapper` + # If we fail on the 10th, continue to throw the exception + limit = 10 + try: + return func(*args, **kwargs) + except OperationalError as exc: + if _i < limit and "Deadlock found" in str(exc): + # Retry on deadlock by recursing into `wrapper` + return wrapper(*args, _i=_i + 1, **kwargs) + # Otherwise, just raise the exception + raise exc + + return wrapper + + +def async_retry_deadlock(func): + from sqlalchemy.exc import OperationalError + + async def wrapper(*args, _i: int = 0, **kwargs): + # Retry 10 times, then raise the exception + # If we fail before the 10th, recurse into `wrapper` + # If we fail on the 10th, continue to throw the exception + limit = 10 + try: + return await func(*args, **kwargs) + except OperationalError as exc: + if _i < limit and "Deadlock found" in str(exc): + # Retry on deadlock by recursing into `wrapper` + return await wrapper(*args, _i=_i + 1, **kwargs) + # Otherwise, just raise the exception + raise exc + + return wrapper + + +def get_sqlalchemy_url(): """ Build an SQLAlchemy URL for use with create_engine. :return: sqlalchemy.engine.url.URL """ + import sqlalchemy + from sqlalchemy.engine.url import URL + + import aurweb.config + constructor = URL - parts = sqlalchemy.__version__.split('.') + parts = sqlalchemy.__version__.split(".") major = int(parts[0]) minor = int(parts[1]) if major == 1 and minor >= 4: # pragma: no cover constructor = URL.create - aur_db_backend = aurweb.config.get('database', 'backend') - if aur_db_backend == 'mysql': + aur_db_backend = aurweb.config.get("database", "backend") + if aur_db_backend == "mysql": param_query = {} port = aurweb.config.get_with_fallback("database", "port", None) if not port: - param_query["unix_socket"] = aurweb.config.get( - "database", "socket") + param_query["unix_socket"] = aurweb.config.get("database", "socket") return constructor( DRIVERS.get(aur_db_backend), - username=aurweb.config.get('database', 'user'), - password=aurweb.config.get_with_fallback('database', 'password', - fallback=None), - host=aurweb.config.get('database', 'host'), + username=aurweb.config.get("database", "user"), + password=aurweb.config.get_with_fallback( + "database", "password", fallback=None + ), + host=aurweb.config.get("database", "host"), database=name(), port=port, - query=param_query + query=param_query, ) - elif aur_db_backend == 'sqlite': + elif aur_db_backend == "sqlite": return constructor( - 'sqlite', - database=aurweb.config.get('database', 'name'), + "sqlite", + database=aurweb.config.get("database", "name"), ) else: - raise ValueError('unsupported database backend') + raise ValueError("unsupported database backend") def sqlite_regexp(regex, item) -> bool: # pragma: no cover - """ Method which mimics SQL's REGEXP for SQLite. """ + """Method which mimics SQL's REGEXP for SQLite.""" + import re + return bool(re.search(regex, str(item))) -def setup_sqlite(engine: Engine) -> None: # pragma: no cover - """ Perform setup for an SQLite engine. """ +def setup_sqlite(engine) -> None: # pragma: no cover + """Perform setup for an SQLite engine.""" + from sqlalchemy import event + @event.listens_for(engine, "connect") def do_begin(conn, record): + import functools + create_deterministic_function = functools.partial( - conn.create_function, - deterministic=True + conn.create_function, deterministic=True ) create_deterministic_function("REGEXP", 2, sqlite_regexp) @@ -227,7 +272,7 @@ def setup_sqlite(engine: Engine) -> None: # pragma: no cover _engines = dict() -def get_engine(dbname: str = None, echo: bool = False) -> Engine: +def get_engine(dbname: str = None, echo: bool = False): """ Return the SQLAlchemy engine for `dbname`. @@ -238,6 +283,8 @@ def get_engine(dbname: str = None, echo: bool = False) -> Engine: :param echo: Flag passed through to sqlalchemy.create_engine :return: SQLAlchemy Engine instance """ + import aurweb.config + if not dbname: dbname = name() @@ -250,11 +297,13 @@ def get_engine(dbname: str = None, echo: bool = False) -> Engine: if is_sqlite: # pragma: no cover connect_args["check_same_thread"] = False - kwargs = { - "echo": echo, - "connect_args": connect_args - } - _engines[dbname] = create_engine(get_sqlalchemy_url(), **kwargs) + kwargs = {"echo": echo, "connect_args": connect_args} + from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor + from sqlalchemy import create_engine + + engine = create_engine(get_sqlalchemy_url(), **kwargs) + SQLAlchemyInstrumentor().instrument(engine=engine) + _engines[dbname] = engine if is_sqlite: # pragma: no cover setup_sqlite(_engines.get(dbname)) @@ -274,7 +323,7 @@ def pop_engine(dbname: str) -> None: def kill_engine() -> None: - """ Close the current session and dispose of the engine. """ + """Close the current session and dispose of the engine.""" dbname = name() session = get_session() @@ -301,12 +350,16 @@ class ConnectionExecutor: _conn = None _paramstyle = None - def __init__(self, conn, backend=aurweb.config.get("database", "backend")): + def __init__(self, conn, backend=None): + import aurweb.config + + backend = backend or aurweb.config.get("database", "backend") self._conn = conn if backend == "mysql": self._paramstyle = "format" elif backend == "sqlite": import sqlite3 + self._paramstyle = sqlite3.paramstyle def paramstyle(self): @@ -314,13 +367,13 @@ class ConnectionExecutor: def execute(self, query, params=()): # pragma: no cover # TODO: SQLite support has been removed in FastAPI. It remains - # here to fund its support for PHP until it is removed. - if self._paramstyle in ('format', 'pyformat'): - query = query.replace('%', '%%').replace('?', '%s') - elif self._paramstyle == 'qmark': + # here to fund its support for the Sharness testsuite. + if self._paramstyle in ("format", "pyformat"): + query = query.replace("%", "%%").replace("?", "%s") + elif self._paramstyle == "qmark": pass else: - raise ValueError('unsupported paramstyle') + raise ValueError("unsupported paramstyle") cur = self._conn.cursor() cur.execute(query, params) @@ -339,30 +392,36 @@ class Connection: _conn = None def __init__(self): - aur_db_backend = aurweb.config.get('database', 'backend') + import aurweb.config - if aur_db_backend == 'mysql': + aur_db_backend = aurweb.config.get("database", "backend") + + if aur_db_backend == "mysql": import MySQLdb - aur_db_host = aurweb.config.get('database', 'host') + + aur_db_host = aurweb.config.get("database", "host") aur_db_name = name() - aur_db_user = aurweb.config.get('database', 'user') - aur_db_pass = aurweb.config.get_with_fallback( - 'database', 'password', str()) - aur_db_socket = aurweb.config.get('database', 'socket') - self._conn = MySQLdb.connect(host=aur_db_host, - user=aur_db_user, - passwd=aur_db_pass, - db=aur_db_name, - unix_socket=aur_db_socket) - elif aur_db_backend == 'sqlite': # pragma: no cover + aur_db_user = aurweb.config.get("database", "user") + aur_db_pass = aurweb.config.get_with_fallback("database", "password", str()) + aur_db_socket = aurweb.config.get("database", "socket") + self._conn = MySQLdb.connect( + host=aur_db_host, + user=aur_db_user, + passwd=aur_db_pass, + db=aur_db_name, + unix_socket=aur_db_socket, + ) + elif aur_db_backend == "sqlite": # pragma: no cover # TODO: SQLite support has been removed in FastAPI. It remains - # here to fund its support for PHP until it is removed. + # here to fund its support for Sharness testsuite. + import math import sqlite3 - aur_db_name = aurweb.config.get('database', 'name') + + aur_db_name = aurweb.config.get("database", "name") self._conn = sqlite3.connect(aur_db_name) self._conn.create_function("POWER", 2, math.pow) else: - raise ValueError('unsupported database backend') + raise ValueError("unsupported database backend") self._conn = ConnectionExecutor(self._conn, aur_db_backend) diff --git a/aurweb/defaults.py b/aurweb/defaults.py index 51072e8f..84d91c55 100644 --- a/aurweb/defaults.py +++ b/aurweb/defaults.py @@ -6,6 +6,9 @@ O = 0 # Default [P]er [P]age PP = 50 +# Default Comments Per Page +COMMENTS_PER_PAGE = 10 + # A whitelist of valid PP values PP_WHITELIST = {50, 100, 250} @@ -14,8 +17,8 @@ RPC_SEARCH_BY = "name-desc" def fallback_pp(per_page: int) -> int: - """ If `per_page` is a valid value in PP_WHITELIST, return it. - Otherwise, return defaults.PP. """ + """If `per_page` is a valid value in PP_WHITELIST, return it. + Otherwise, return defaults.PP.""" if per_page not in PP_WHITELIST: return PP return per_page diff --git a/aurweb/exceptions.py b/aurweb/exceptions.py index 30a3df08..e24eb607 100644 --- a/aurweb/exceptions.py +++ b/aurweb/exceptions.py @@ -1,5 +1,4 @@ import functools - from typing import Any, Callable import fastapi @@ -19,61 +18,61 @@ class BannedException(AurwebException): class PermissionDeniedException(AurwebException): def __init__(self, user): - msg = 'permission denied: {:s}'.format(user) + msg = "permission denied: {:s}".format(user) super(PermissionDeniedException, self).__init__(msg) class BrokenUpdateHookException(AurwebException): def __init__(self, cmd): - msg = 'broken update hook: {:s}'.format(cmd) + msg = "broken update hook: {:s}".format(cmd) super(BrokenUpdateHookException, self).__init__(msg) class InvalidUserException(AurwebException): def __init__(self, user): - msg = 'unknown user: {:s}'.format(user) + msg = "unknown user: {:s}".format(user) super(InvalidUserException, self).__init__(msg) class InvalidPackageBaseException(AurwebException): def __init__(self, pkgbase): - msg = 'package base not found: {:s}'.format(pkgbase) + msg = "package base not found: {:s}".format(pkgbase) super(InvalidPackageBaseException, self).__init__(msg) class InvalidRepositoryNameException(AurwebException): def __init__(self, pkgbase): - msg = 'invalid repository name: {:s}'.format(pkgbase) + msg = "invalid repository name: {:s}".format(pkgbase) super(InvalidRepositoryNameException, self).__init__(msg) class PackageBaseExistsException(AurwebException): def __init__(self, pkgbase): - msg = 'package base already exists: {:s}'.format(pkgbase) + msg = "package base already exists: {:s}".format(pkgbase) super(PackageBaseExistsException, self).__init__(msg) class InvalidReasonException(AurwebException): def __init__(self, reason): - msg = 'invalid reason: {:s}'.format(reason) + msg = "invalid reason: {:s}".format(reason) super(InvalidReasonException, self).__init__(msg) class InvalidCommentException(AurwebException): def __init__(self, comment): - msg = 'comment is too short: {:s}'.format(comment) + msg = "comment is too short: {:s}".format(comment) super(InvalidCommentException, self).__init__(msg) class AlreadyVotedException(AurwebException): def __init__(self, comment): - msg = 'already voted for package base: {:s}'.format(comment) + msg = "already voted for package base: {:s}".format(comment) super(AlreadyVotedException, self).__init__(msg) class NotVotedException(AurwebException): def __init__(self, comment): - msg = 'missing vote for package base: {:s}'.format(comment) + msg = "missing vote for package base: {:s}".format(comment) super(NotVotedException, self).__init__(msg) @@ -109,4 +108,5 @@ def handle_form_exceptions(route: Callable) -> fastapi.Response: async def wrapper(request: fastapi.Request, *args, **kwargs): request.state.form_data = await request.form() return await route(request, *args, **kwargs) + return wrapper diff --git a/aurweb/filters.py b/aurweb/filters.py index 45cb6d83..f39bd560 100644 --- a/aurweb/filters.py +++ b/aurweb/filters.py @@ -1,26 +1,23 @@ import copy import math - -from datetime import datetime -from typing import Any, Dict, Union +from datetime import UTC, datetime +from typing import Any, Union from urllib.parse import quote_plus, urlencode from zoneinfo import ZoneInfo import fastapi import paginate - from jinja2 import pass_context +from jinja2.filters import do_format import aurweb.models - from aurweb import config, l10n from aurweb.templates import register_filter, register_function @register_filter("pager_nav") @pass_context -def pager_nav(context: Dict[str, Any], - page: int, total: int, prefix: str) -> str: +def pager_nav(context: dict[str, Any], page: int, total: int, prefix: str) -> str: page = int(page) # Make sure this is an int. pp = context.get("PP", 50) @@ -43,10 +40,9 @@ def pager_nav(context: Dict[str, Any], return f"{prefix}?{qs}" # Use the paginate module to produce our linkage. - pager = paginate.Page([], page=page + 1, - items_per_page=pp, - item_count=total, - url_maker=create_url) + pager = paginate.Page( + [], page=page + 1, items_per_page=pp, item_count=total, url_maker=create_url + ) return pager.pager( link_attr={"class": "page"}, @@ -56,7 +52,8 @@ def pager_nav(context: Dict[str, Any], symbol_first="« First", symbol_previous="‹ Previous", symbol_next="Next ›", - symbol_last="Last »") + symbol_last="Last »", + ) @register_function("config_getint") @@ -71,17 +68,16 @@ def do_round(f: float) -> int: @register_filter("tr") @pass_context -def tr(context: Dict[str, Any], value: str): - """ A translation filter; example: {{ "Hello" | tr("de") }}. """ +def tr(context: dict[str, Any], value: str): + """A translation filter; example: {{ "Hello" | tr("de") }}.""" _ = l10n.get_translator_for_request(context.get("request")) return _(value) @register_filter("tn") @pass_context -def tn(context: Dict[str, Any], count: int, - singular: str, plural: str) -> str: - """ A singular and plural translation filter. +def tn(context: dict[str, Any], count: int, singular: str, plural: str) -> str: + """A singular and plural translation filter. Example: {{ some_integer | tn("singular %d", "plural %d") }} @@ -98,7 +94,7 @@ def tn(context: Dict[str, Any], count: int, @register_filter("dt") def timestamp_to_datetime(timestamp: int): - return datetime.utcfromtimestamp(int(timestamp)) + return datetime.fromtimestamp(timestamp, UTC) @register_filter("as_timezone") @@ -107,8 +103,8 @@ def as_timezone(dt: datetime, timezone: str): @register_filter("extend_query") -def extend_query(query: Dict[str, Any], *additions) -> Dict[str, Any]: - """ Add additional key value pairs to query. """ +def extend_query(query: dict[str, Any], *additions) -> dict[str, Any]: + """Add additional key value pairs to query.""" q = copy.copy(query) for k, v in list(additions): q[k] = v @@ -116,26 +112,26 @@ def extend_query(query: Dict[str, Any], *additions) -> Dict[str, Any]: @register_filter("urlencode") -def to_qs(query: Dict[str, Any]) -> str: +def to_qs(query: dict[str, Any]) -> str: return urlencode(query, doseq=True) @register_filter("get_vote") def get_vote(voteinfo, request: fastapi.Request): - from aurweb.models import TUVote - return voteinfo.tu_votes.filter(TUVote.User == request.user).first() + from aurweb.models import Vote + + return voteinfo.votes.filter(Vote.User == request.user).first() @register_filter("number_format") def number_format(value: float, places: int): - """ A converter function similar to PHP's number_format. """ + """A converter function similar to PHP's number_format.""" return f"{value:.{places}f}" @register_filter("account_url") @pass_context -def account_url(context: Dict[str, Any], - user: "aurweb.models.user.User") -> str: +def account_url(context: dict[str, Any], user: "aurweb.models.user.User") -> str: base = aurweb.config.get("options", "aur_location") return f"{base}/account/{user.Username}" @@ -152,8 +148,7 @@ def ceil(*args, **kwargs) -> int: @register_function("date_strftime") @pass_context -def date_strftime(context: Dict[str, Any], dt: Union[int, datetime], fmt: str) \ - -> str: +def date_strftime(context: dict[str, Any], dt: Union[int, datetime], fmt: str) -> str: if isinstance(dt, int): dt = timestamp_to_datetime(dt) tz = context.get("timezone") @@ -162,11 +157,25 @@ def date_strftime(context: Dict[str, Any], dt: Union[int, datetime], fmt: str) \ @register_function("date_display") @pass_context -def date_display(context: Dict[str, Any], dt: Union[int, datetime]) -> str: +def date_display(context: dict[str, Any], dt: Union[int, datetime]) -> str: return date_strftime(context, dt, "%Y-%m-%d (%Z)") @register_function("datetime_display") @pass_context -def datetime_display(context: Dict[str, Any], dt: Union[int, datetime]) -> str: +def datetime_display(context: dict[str, Any], dt: Union[int, datetime]) -> str: return date_strftime(context, dt, "%Y-%m-%d %H:%M (%Z)") + + +@register_filter("format") +def safe_format(value: str, *args: Any, **kwargs: Any) -> str: + """Wrapper for jinja2 format function to perform additional checks.""" + + # If we don't have anything to be formatted, just return the value. + # We have some translations that do not contain placeholders for replacement. + # In these cases the jinja2 function is throwing an error: + # "TypeError: not all arguments converted during string formatting" + if "%" not in value: + return value + + return do_format(value, *args, **kwargs) diff --git a/aurweb/git/auth.py b/aurweb/git/auth.py index abecd276..759fce89 100755 --- a/aurweb/git/auth.py +++ b/aurweb/git/auth.py @@ -9,12 +9,12 @@ import aurweb.db def format_command(env_vars, command, ssh_opts, ssh_key): - environment = '' + environment = "" for key, var in env_vars.items(): - environment += '{}={} '.format(key, shlex.quote(var)) + environment += "{}={} ".format(key, shlex.quote(var)) command = shlex.quote(command) - command = '{}{}'.format(environment, command) + command = "{}{}".format(environment, command) # The command is being substituted into an authorized_keys line below, # so we need to escape the double quotes. @@ -24,10 +24,10 @@ def format_command(env_vars, command, ssh_opts, ssh_key): def main(): - valid_keytypes = aurweb.config.get('auth', 'valid-keytypes').split() - username_regex = aurweb.config.get('auth', 'username-regex') - git_serve_cmd = aurweb.config.get('auth', 'git-serve-cmd') - ssh_opts = aurweb.config.get('auth', 'ssh-options') + valid_keytypes = aurweb.config.get("auth", "valid-keytypes").split() + username_regex = aurweb.config.get("auth", "username-regex") + git_serve_cmd = aurweb.config.get("auth", "git-serve-cmd") + ssh_opts = aurweb.config.get("auth", "ssh-options") keytype = sys.argv[1] keytext = sys.argv[2] @@ -36,11 +36,13 @@ def main(): conn = aurweb.db.Connection() - cur = conn.execute("SELECT Users.Username, Users.AccountTypeID FROM Users " - "INNER JOIN SSHPubKeys ON SSHPubKeys.UserID = Users.ID " - "WHERE SSHPubKeys.PubKey = ? AND Users.Suspended = 0 " - "AND NOT Users.Passwd = ''", - (keytype + " " + keytext,)) + cur = conn.execute( + "SELECT Users.Username, Users.AccountTypeID FROM Users " + "INNER JOIN SSHPubKeys ON SSHPubKeys.UserID = Users.ID " + "WHERE SSHPubKeys.PubKey = ? AND Users.Suspended = 0 " + "AND NOT Users.Passwd = ''", + (keytype + " " + keytext,), + ) row = cur.fetchone() if not row or cur.fetchone(): @@ -51,13 +53,13 @@ def main(): exit(1) env_vars = { - 'AUR_USER': user, - 'AUR_PRIVILEGED': '1' if account_type > 1 else '0', + "AUR_USER": user, + "AUR_PRIVILEGED": "1" if account_type > 1 else "0", } - key = keytype + ' ' + keytext + key = keytype + " " + keytext print(format_command(env_vars, git_serve_cmd, ssh_opts, key)) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/aurweb/git/serve.py b/aurweb/git/serve.py index b91f1a13..5888c6b4 100755 --- a/aurweb/git/serve.py +++ b/aurweb/git/serve.py @@ -11,16 +11,16 @@ import aurweb.config import aurweb.db import aurweb.exceptions -notify_cmd = aurweb.config.get('notifications', 'notify-cmd') +notify_cmd = aurweb.config.get("notifications", "notify-cmd") -repo_path = aurweb.config.get('serve', 'repo-path') -repo_regex = aurweb.config.get('serve', 'repo-regex') -git_shell_cmd = aurweb.config.get('serve', 'git-shell-cmd') -git_update_cmd = aurweb.config.get('serve', 'git-update-cmd') -ssh_cmdline = aurweb.config.get('serve', 'ssh-cmdline') +repo_path = aurweb.config.get("serve", "repo-path") +repo_regex = aurweb.config.get("serve", "repo-regex") +git_shell_cmd = aurweb.config.get("serve", "git-shell-cmd") +git_update_cmd = aurweb.config.get("serve", "git-update-cmd") +ssh_cmdline = aurweb.config.get("serve", "ssh-cmdline") -enable_maintenance = aurweb.config.getboolean('options', 'enable-maintenance') -maintenance_exc = aurweb.config.get('options', 'maintenance-exceptions').split() +enable_maintenance = aurweb.config.getboolean("options", "enable-maintenance") +maintenance_exc = aurweb.config.get("options", "maintenance-exceptions").split() def pkgbase_from_name(pkgbase): @@ -43,14 +43,16 @@ def list_repos(user): if userid == 0: raise aurweb.exceptions.InvalidUserException(user) - cur = conn.execute("SELECT Name, PackagerUID FROM PackageBases " + - "WHERE MaintainerUID = ?", [userid]) + cur = conn.execute( + "SELECT Name, PackagerUID FROM PackageBases " + "WHERE MaintainerUID = ?", + [userid], + ) for row in cur: - print((' ' if row[1] else '*') + row[0]) + print((" " if row[1] else "*") + row[0]) conn.close() -def create_pkgbase(pkgbase, user): +def validate_pkgbase(pkgbase, user): if not re.match(repo_regex, pkgbase): raise aurweb.exceptions.InvalidRepositoryNameException(pkgbase) if pkgbase_exists(pkgbase): @@ -60,23 +62,12 @@ def create_pkgbase(pkgbase, user): cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [user]) userid = cur.fetchone()[0] + + conn.close() + if userid == 0: raise aurweb.exceptions.InvalidUserException(user) - now = int(time.time()) - cur = conn.execute("INSERT INTO PackageBases (Name, SubmittedTS, " + - "ModifiedTS, SubmitterUID, MaintainerUID, " + - "FlaggerComment) VALUES (?, ?, ?, ?, ?, '')", - [pkgbase, now, now, userid, userid]) - pkgbase_id = cur.lastrowid - - cur = conn.execute("INSERT INTO PackageNotifications " + - "(PackageBaseID, UserID) VALUES (?, ?)", - [pkgbase_id, userid]) - - conn.commit() - conn.close() - def pkgbase_adopt(pkgbase, user, privileged): pkgbase_id = pkgbase_from_name(pkgbase) @@ -85,8 +76,10 @@ def pkgbase_adopt(pkgbase, user, privileged): conn = aurweb.db.Connection() - cur = conn.execute("SELECT ID FROM PackageBases WHERE ID = ? AND " + - "MaintainerUID IS NULL", [pkgbase_id]) + cur = conn.execute( + "SELECT ID FROM PackageBases WHERE ID = ? AND " + "MaintainerUID IS NULL", + [pkgbase_id], + ) if not privileged and not cur.fetchone(): raise aurweb.exceptions.PermissionDeniedException(user) @@ -95,19 +88,25 @@ def pkgbase_adopt(pkgbase, user, privileged): if userid == 0: raise aurweb.exceptions.InvalidUserException(user) - cur = conn.execute("UPDATE PackageBases SET MaintainerUID = ? " + - "WHERE ID = ?", [userid, pkgbase_id]) + cur = conn.execute( + "UPDATE PackageBases SET MaintainerUID = ? " + "WHERE ID = ?", + [userid, pkgbase_id], + ) - cur = conn.execute("SELECT COUNT(*) FROM PackageNotifications WHERE " + - "PackageBaseID = ? AND UserID = ?", - [pkgbase_id, userid]) + cur = conn.execute( + "SELECT COUNT(*) FROM PackageNotifications WHERE " + + "PackageBaseID = ? AND UserID = ?", + [pkgbase_id, userid], + ) if cur.fetchone()[0] == 0: - cur = conn.execute("INSERT INTO PackageNotifications " + - "(PackageBaseID, UserID) VALUES (?, ?)", - [pkgbase_id, userid]) + cur = conn.execute( + "INSERT INTO PackageNotifications " + + "(PackageBaseID, UserID) VALUES (?, ?)", + [pkgbase_id, userid], + ) conn.commit() - subprocess.Popen((notify_cmd, 'adopt', str(userid), str(pkgbase_id))) + subprocess.Popen((notify_cmd, "adopt", str(userid), str(pkgbase_id))) conn.close() @@ -115,13 +114,16 @@ def pkgbase_adopt(pkgbase, user, privileged): def pkgbase_get_comaintainers(pkgbase): conn = aurweb.db.Connection() - cur = conn.execute("SELECT UserName FROM PackageComaintainers " + - "INNER JOIN Users " + - "ON Users.ID = PackageComaintainers.UsersID " + - "INNER JOIN PackageBases " + - "ON PackageBases.ID = PackageComaintainers.PackageBaseID " + - "WHERE PackageBases.Name = ? " + - "ORDER BY Priority ASC", [pkgbase]) + cur = conn.execute( + "SELECT UserName FROM PackageComaintainers " + + "INNER JOIN Users " + + "ON Users.ID = PackageComaintainers.UsersID " + + "INNER JOIN PackageBases " + + "ON PackageBases.ID = PackageComaintainers.PackageBaseID " + + "WHERE PackageBases.Name = ? " + + "ORDER BY Priority ASC", + [pkgbase], + ) return [row[0] for row in cur.fetchall()] @@ -140,8 +142,7 @@ def pkgbase_set_comaintainers(pkgbase, userlist, user, privileged): uids_old = set() for olduser in userlist_old: - cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", - [olduser]) + cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [olduser]) userid = cur.fetchone()[0] if userid == 0: raise aurweb.exceptions.InvalidUserException(user) @@ -149,8 +150,7 @@ def pkgbase_set_comaintainers(pkgbase, userlist, user, privileged): uids_new = set() for newuser in userlist: - cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", - [newuser]) + cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", [newuser]) userid = cur.fetchone()[0] if userid == 0: raise aurweb.exceptions.InvalidUserException(user) @@ -162,24 +162,33 @@ def pkgbase_set_comaintainers(pkgbase, userlist, user, privileged): i = 1 for userid in uids_new: if userid in uids_add: - cur = conn.execute("INSERT INTO PackageComaintainers " + - "(PackageBaseID, UsersID, Priority) " + - "VALUES (?, ?, ?)", [pkgbase_id, userid, i]) - subprocess.Popen((notify_cmd, 'comaintainer-add', str(userid), - str(pkgbase_id))) + cur = conn.execute( + "INSERT INTO PackageComaintainers " + + "(PackageBaseID, UsersID, Priority) " + + "VALUES (?, ?, ?)", + [pkgbase_id, userid, i], + ) + subprocess.Popen( + (notify_cmd, "comaintainer-add", str(userid), str(pkgbase_id)) + ) else: - cur = conn.execute("UPDATE PackageComaintainers " + - "SET Priority = ? " + - "WHERE PackageBaseID = ? AND UsersID = ?", - [i, pkgbase_id, userid]) + cur = conn.execute( + "UPDATE PackageComaintainers " + + "SET Priority = ? " + + "WHERE PackageBaseID = ? AND UsersID = ?", + [i, pkgbase_id, userid], + ) i += 1 for userid in uids_rem: - cur = conn.execute("DELETE FROM PackageComaintainers " + - "WHERE PackageBaseID = ? AND UsersID = ?", - [pkgbase_id, userid]) - subprocess.Popen((notify_cmd, 'comaintainer-remove', - str(userid), str(pkgbase_id))) + cur = conn.execute( + "DELETE FROM PackageComaintainers " + + "WHERE PackageBaseID = ? AND UsersID = ?", + [pkgbase_id, userid], + ) + subprocess.Popen( + (notify_cmd, "comaintainer-remove", str(userid), str(pkgbase_id)) + ) conn.commit() conn.close() @@ -188,18 +197,21 @@ def pkgbase_set_comaintainers(pkgbase, userlist, user, privileged): def pkgreq_by_pkgbase(pkgbase_id, reqtype): conn = aurweb.db.Connection() - cur = conn.execute("SELECT PackageRequests.ID FROM PackageRequests " + - "INNER JOIN RequestTypes ON " + - "RequestTypes.ID = PackageRequests.ReqTypeID " + - "WHERE PackageRequests.Status = 0 " + - "AND PackageRequests.PackageBaseID = ? " + - "AND RequestTypes.Name = ?", [pkgbase_id, reqtype]) + cur = conn.execute( + "SELECT PackageRequests.ID FROM PackageRequests " + + "INNER JOIN RequestTypes ON " + + "RequestTypes.ID = PackageRequests.ReqTypeID " + + "WHERE PackageRequests.Status = 0 " + + "AND PackageRequests.PackageBaseID = ? " + + "AND RequestTypes.Name = ?", + [pkgbase_id, reqtype], + ) return [row[0] for row in cur.fetchall()] def pkgreq_close(reqid, user, reason, comments, autoclose=False): - statusmap = {'accepted': 2, 'rejected': 3} + statusmap = {"accepted": 2, "rejected": 3} if reason not in statusmap: raise aurweb.exceptions.InvalidReasonException(reason) status = statusmap[reason] @@ -215,16 +227,20 @@ def pkgreq_close(reqid, user, reason, comments, autoclose=False): raise aurweb.exceptions.InvalidUserException(user) now = int(time.time()) - conn.execute("UPDATE PackageRequests SET Status = ?, ClosedTS = ?, " + - "ClosedUID = ?, ClosureComment = ? " + - "WHERE ID = ?", [status, now, userid, comments, reqid]) + conn.execute( + "UPDATE PackageRequests SET Status = ?, ClosedTS = ?, " + + "ClosedUID = ?, ClosureComment = ? " + + "WHERE ID = ?", + [status, now, userid, comments, reqid], + ) conn.commit() conn.close() if not userid: userid = 0 - subprocess.Popen((notify_cmd, 'request-close', str(userid), str(reqid), - reason)).wait() + subprocess.Popen( + (notify_cmd, "request-close", str(userid), str(reqid), reason) + ).wait() def pkgbase_disown(pkgbase, user, privileged): @@ -239,9 +255,9 @@ def pkgbase_disown(pkgbase, user, privileged): # TODO: Support disowning package bases via package request. # Scan through pending orphan requests and close them. - comment = 'The user {:s} disowned the package.'.format(user) - for reqid in pkgreq_by_pkgbase(pkgbase_id, 'orphan'): - pkgreq_close(reqid, user, 'accepted', comment, True) + comment = "The user {:s} disowned the package.".format(user) + for reqid in pkgreq_by_pkgbase(pkgbase_id, "orphan"): + pkgreq_close(reqid, user, "accepted", comment, True) comaintainers = [] new_maintainer_userid = None @@ -249,19 +265,22 @@ def pkgbase_disown(pkgbase, user, privileged): conn = aurweb.db.Connection() # Make the first co-maintainer the new maintainer, unless the action was - # enforced by a Trusted User. + # enforced by a Package Maintainer. if initialized_by_owner: comaintainers = pkgbase_get_comaintainers(pkgbase) if len(comaintainers) > 0: new_maintainer = comaintainers[0] - cur = conn.execute("SELECT ID FROM Users WHERE Username = ?", - [new_maintainer]) + cur = conn.execute( + "SELECT ID FROM Users WHERE Username = ?", [new_maintainer] + ) new_maintainer_userid = cur.fetchone()[0] comaintainers.remove(new_maintainer) pkgbase_set_comaintainers(pkgbase, comaintainers, user, privileged) - cur = conn.execute("UPDATE PackageBases SET MaintainerUID = ? " + - "WHERE ID = ?", [new_maintainer_userid, pkgbase_id]) + cur = conn.execute( + "UPDATE PackageBases SET MaintainerUID = ? " + "WHERE ID = ?", + [new_maintainer_userid, pkgbase_id], + ) conn.commit() @@ -270,7 +289,7 @@ def pkgbase_disown(pkgbase, user, privileged): if userid == 0: raise aurweb.exceptions.InvalidUserException(user) - subprocess.Popen((notify_cmd, 'disown', str(userid), str(pkgbase_id))) + subprocess.Popen((notify_cmd, "disown", str(userid), str(pkgbase_id))) conn.close() @@ -290,14 +309,16 @@ def pkgbase_flag(pkgbase, user, comment): raise aurweb.exceptions.InvalidUserException(user) now = int(time.time()) - conn.execute("UPDATE PackageBases SET " + - "OutOfDateTS = ?, FlaggerUID = ?, FlaggerComment = ? " + - "WHERE ID = ? AND OutOfDateTS IS NULL", - [now, userid, comment, pkgbase_id]) + conn.execute( + "UPDATE PackageBases SET " + + "OutOfDateTS = ?, FlaggerUID = ?, FlaggerComment = ? " + + "WHERE ID = ? AND OutOfDateTS IS NULL", + [now, userid, comment, pkgbase_id], + ) conn.commit() - subprocess.Popen((notify_cmd, 'flag', str(userid), str(pkgbase_id))) + subprocess.Popen((notify_cmd, "flag", str(userid), str(pkgbase_id))) def pkgbase_unflag(pkgbase, user): @@ -313,12 +334,15 @@ def pkgbase_unflag(pkgbase, user): raise aurweb.exceptions.InvalidUserException(user) if user in pkgbase_get_comaintainers(pkgbase): - conn.execute("UPDATE PackageBases SET OutOfDateTS = NULL " + - "WHERE ID = ?", [pkgbase_id]) + conn.execute( + "UPDATE PackageBases SET OutOfDateTS = NULL " + "WHERE ID = ?", [pkgbase_id] + ) else: - conn.execute("UPDATE PackageBases SET OutOfDateTS = NULL " + - "WHERE ID = ? AND (MaintainerUID = ? OR FlaggerUID = ?)", - [pkgbase_id, userid, userid]) + conn.execute( + "UPDATE PackageBases SET OutOfDateTS = NULL " + + "WHERE ID = ? AND (MaintainerUID = ? OR FlaggerUID = ?)", + [pkgbase_id, userid, userid], + ) conn.commit() @@ -335,17 +359,24 @@ def pkgbase_vote(pkgbase, user): if userid == 0: raise aurweb.exceptions.InvalidUserException(user) - cur = conn.execute("SELECT COUNT(*) FROM PackageVotes " + - "WHERE UsersID = ? AND PackageBaseID = ?", - [userid, pkgbase_id]) + cur = conn.execute( + "SELECT COUNT(*) FROM PackageVotes " + + "WHERE UsersID = ? AND PackageBaseID = ?", + [userid, pkgbase_id], + ) if cur.fetchone()[0] > 0: raise aurweb.exceptions.AlreadyVotedException(pkgbase) now = int(time.time()) - conn.execute("INSERT INTO PackageVotes (UsersID, PackageBaseID, VoteTS) " + - "VALUES (?, ?, ?)", [userid, pkgbase_id, now]) - conn.execute("UPDATE PackageBases SET NumVotes = NumVotes + 1 " + - "WHERE ID = ?", [pkgbase_id]) + conn.execute( + "INSERT INTO PackageVotes (UsersID, PackageBaseID, VoteTS) " + + "VALUES (?, ?, ?)", + [userid, pkgbase_id, now], + ) + conn.execute( + "UPDATE PackageBases SET NumVotes = NumVotes + 1 " + "WHERE ID = ?", + [pkgbase_id], + ) conn.commit() @@ -361,16 +392,22 @@ def pkgbase_unvote(pkgbase, user): if userid == 0: raise aurweb.exceptions.InvalidUserException(user) - cur = conn.execute("SELECT COUNT(*) FROM PackageVotes " + - "WHERE UsersID = ? AND PackageBaseID = ?", - [userid, pkgbase_id]) + cur = conn.execute( + "SELECT COUNT(*) FROM PackageVotes " + + "WHERE UsersID = ? AND PackageBaseID = ?", + [userid, pkgbase_id], + ) if cur.fetchone()[0] == 0: raise aurweb.exceptions.NotVotedException(pkgbase) - conn.execute("DELETE FROM PackageVotes WHERE UsersID = ? AND " + - "PackageBaseID = ?", [userid, pkgbase_id]) - conn.execute("UPDATE PackageBases SET NumVotes = NumVotes - 1 " + - "WHERE ID = ?", [pkgbase_id]) + conn.execute( + "DELETE FROM PackageVotes WHERE UsersID = ? AND " + "PackageBaseID = ?", + [userid, pkgbase_id], + ) + conn.execute( + "UPDATE PackageBases SET NumVotes = NumVotes - 1 " + "WHERE ID = ?", + [pkgbase_id], + ) conn.commit() @@ -381,11 +418,12 @@ def pkgbase_set_keywords(pkgbase, keywords): conn = aurweb.db.Connection() - conn.execute("DELETE FROM PackageKeywords WHERE PackageBaseID = ?", - [pkgbase_id]) + conn.execute("DELETE FROM PackageKeywords WHERE PackageBaseID = ?", [pkgbase_id]) for keyword in keywords: - conn.execute("INSERT INTO PackageKeywords (PackageBaseID, Keyword) " + - "VALUES (?, ?)", [pkgbase_id, keyword]) + conn.execute( + "INSERT INTO PackageKeywords (PackageBaseID, Keyword) " + "VALUES (?, ?)", + [pkgbase_id, keyword], + ) conn.commit() conn.close() @@ -394,24 +432,30 @@ def pkgbase_set_keywords(pkgbase, keywords): def pkgbase_has_write_access(pkgbase, user): conn = aurweb.db.Connection() - cur = conn.execute("SELECT COUNT(*) FROM PackageBases " + - "LEFT JOIN PackageComaintainers " + - "ON PackageComaintainers.PackageBaseID = PackageBases.ID " + - "INNER JOIN Users " + - "ON Users.ID = PackageBases.MaintainerUID " + - "OR PackageBases.MaintainerUID IS NULL " + - "OR Users.ID = PackageComaintainers.UsersID " + - "WHERE Name = ? AND Username = ?", [pkgbase, user]) + cur = conn.execute( + "SELECT COUNT(*) FROM PackageBases " + + "LEFT JOIN PackageComaintainers " + + "ON PackageComaintainers.PackageBaseID = PackageBases.ID " + + "INNER JOIN Users " + + "ON Users.ID = PackageBases.MaintainerUID " + + "OR PackageBases.MaintainerUID IS NULL " + + "OR Users.ID = PackageComaintainers.UsersID " + + "WHERE Name = ? AND Username = ?", + [pkgbase, user], + ) return cur.fetchone()[0] > 0 def pkgbase_has_full_access(pkgbase, user): conn = aurweb.db.Connection() - cur = conn.execute("SELECT COUNT(*) FROM PackageBases " + - "INNER JOIN Users " + - "ON Users.ID = PackageBases.MaintainerUID " + - "WHERE Name = ? AND Username = ?", [pkgbase, user]) + cur = conn.execute( + "SELECT COUNT(*) FROM PackageBases " + + "INNER JOIN Users " + + "ON Users.ID = PackageBases.MaintainerUID " + + "WHERE Name = ? AND Username = ?", + [pkgbase, user], + ) return cur.fetchone()[0] > 0 @@ -419,9 +463,11 @@ def log_ssh_login(user, remote_addr): conn = aurweb.db.Connection() now = int(time.time()) - conn.execute("UPDATE Users SET LastSSHLogin = ?, " + - "LastSSHLoginIPAddress = ? WHERE Username = ?", - [now, remote_addr, user]) + conn.execute( + "UPDATE Users SET LastSSHLogin = ?, " + + "LastSSHLoginIPAddress = ? WHERE Username = ?", + [now, remote_addr, user], + ) conn.commit() conn.close() @@ -430,8 +476,7 @@ def log_ssh_login(user, remote_addr): def bans_match(remote_addr): conn = aurweb.db.Connection() - cur = conn.execute("SELECT COUNT(*) FROM Bans WHERE IPAddress = ?", - [remote_addr]) + cur = conn.execute("SELECT COUNT(*) FROM Bans WHERE IPAddress = ?", [remote_addr]) return cur.fetchone()[0] > 0 @@ -458,13 +503,13 @@ def usage(cmds): def checkarg_atleast(cmdargv, *argdesc): if len(cmdargv) - 1 < len(argdesc): - msg = 'missing {:s}'.format(argdesc[len(cmdargv) - 1]) + msg = "missing {:s}".format(argdesc[len(cmdargv) - 1]) raise aurweb.exceptions.InvalidArgumentsException(msg) def checkarg_atmost(cmdargv, *argdesc): if len(cmdargv) - 1 > len(argdesc): - raise aurweb.exceptions.InvalidArgumentsException('too many arguments') + raise aurweb.exceptions.InvalidArgumentsException("too many arguments") def checkarg(cmdargv, *argdesc): @@ -480,23 +525,23 @@ def serve(action, cmdargv, user, privileged, remote_addr): # noqa: C901 raise aurweb.exceptions.BannedException log_ssh_login(user, remote_addr) - if action == 'git' and cmdargv[1] in ('upload-pack', 'receive-pack'): - action = action + '-' + cmdargv[1] + if action == "git" and cmdargv[1] in ("upload-pack", "receive-pack"): + action = action + "-" + cmdargv[1] del cmdargv[1] - if action == 'git-upload-pack' or action == 'git-receive-pack': - checkarg(cmdargv, 'path') + if action == "git-upload-pack" or action == "git-receive-pack": + checkarg(cmdargv, "path") - path = cmdargv[1].rstrip('/') - if not path.startswith('/'): - path = '/' + path - if not path.endswith('.git'): - path = path + '.git' + path = cmdargv[1].rstrip("/") + if not path.startswith("/"): + path = "/" + path + if not path.endswith(".git"): + path = path + ".git" pkgbase = path[1:-4] if not re.match(repo_regex, pkgbase): raise aurweb.exceptions.InvalidRepositoryNameException(pkgbase) - if action == 'git-receive-pack' and pkgbase_exists(pkgbase): + if action == "git-receive-pack" and pkgbase_exists(pkgbase): if not privileged and not pkgbase_has_write_access(pkgbase, user): raise aurweb.exceptions.PermissionDeniedException(user) @@ -507,65 +552,60 @@ def serve(action, cmdargv, user, privileged, remote_addr): # noqa: C901 os.environ["AUR_PKGBASE"] = pkgbase os.environ["GIT_NAMESPACE"] = pkgbase cmd = action + " '" + repo_path + "'" - os.execl(git_shell_cmd, git_shell_cmd, '-c', cmd) - elif action == 'set-keywords': - checkarg_atleast(cmdargv, 'repository name') + os.execl(git_shell_cmd, git_shell_cmd, "-c", cmd) + elif action == "set-keywords": + checkarg_atleast(cmdargv, "repository name") pkgbase_set_keywords(cmdargv[1], cmdargv[2:]) - elif action == 'list-repos': + elif action == "list-repos": checkarg(cmdargv) list_repos(user) - elif action == 'setup-repo': - checkarg(cmdargv, 'repository name') - warn('{:s} is deprecated. ' - 'Use `git push` to create new repositories.'.format(action)) - create_pkgbase(cmdargv[1], user) - elif action == 'restore': - checkarg(cmdargv, 'repository name') + elif action == "restore": + checkarg(cmdargv, "repository name") pkgbase = cmdargv[1] - create_pkgbase(pkgbase, user) + validate_pkgbase(pkgbase, user) os.environ["AUR_USER"] = user os.environ["AUR_PKGBASE"] = pkgbase - os.execl(git_update_cmd, git_update_cmd, 'restore') - elif action == 'adopt': - checkarg(cmdargv, 'repository name') + os.execl(git_update_cmd, git_update_cmd, "restore") + elif action == "adopt": + checkarg(cmdargv, "repository name") pkgbase = cmdargv[1] pkgbase_adopt(pkgbase, user, privileged) - elif action == 'disown': - checkarg(cmdargv, 'repository name') + elif action == "disown": + checkarg(cmdargv, "repository name") pkgbase = cmdargv[1] pkgbase_disown(pkgbase, user, privileged) - elif action == 'flag': - checkarg(cmdargv, 'repository name', 'comment') + elif action == "flag": + checkarg(cmdargv, "repository name", "comment") pkgbase = cmdargv[1] comment = cmdargv[2] pkgbase_flag(pkgbase, user, comment) - elif action == 'unflag': - checkarg(cmdargv, 'repository name') + elif action == "unflag": + checkarg(cmdargv, "repository name") pkgbase = cmdargv[1] pkgbase_unflag(pkgbase, user) - elif action == 'vote': - checkarg(cmdargv, 'repository name') + elif action == "vote": + checkarg(cmdargv, "repository name") pkgbase = cmdargv[1] pkgbase_vote(pkgbase, user) - elif action == 'unvote': - checkarg(cmdargv, 'repository name') + elif action == "unvote": + checkarg(cmdargv, "repository name") pkgbase = cmdargv[1] pkgbase_unvote(pkgbase, user) - elif action == 'set-comaintainers': - checkarg_atleast(cmdargv, 'repository name') + elif action == "set-comaintainers": + checkarg_atleast(cmdargv, "repository name") pkgbase = cmdargv[1] userlist = cmdargv[2:] pkgbase_set_comaintainers(pkgbase, userlist, user, privileged) - elif action == 'help': + elif action == "help": cmds = { "adopt ": "Adopt a package base.", "disown ": "Disown a package base.", @@ -575,7 +615,6 @@ def serve(action, cmdargv, user, privileged, remote_addr): # noqa: C901 "restore ": "Restore a deleted package base.", "set-comaintainers [...]": "Set package base co-maintainers.", "set-keywords [...]": "Change package base keywords.", - "setup-repo ": "Create a repository (deprecated).", "unflag ": "Remove out-of-date flag from a package base.", "unvote ": "Remove vote from a package base.", "vote ": "Vote for a package base.", @@ -584,21 +623,21 @@ def serve(action, cmdargv, user, privileged, remote_addr): # noqa: C901 } usage(cmds) else: - msg = 'invalid command: {:s}'.format(action) + msg = "invalid command: {:s}".format(action) raise aurweb.exceptions.InvalidArgumentsException(msg) def main(): - user = os.environ.get('AUR_USER') - privileged = (os.environ.get('AUR_PRIVILEGED', '0') == '1') - ssh_cmd = os.environ.get('SSH_ORIGINAL_COMMAND') - ssh_client = os.environ.get('SSH_CLIENT') + user = os.environ.get("AUR_USER") + privileged = os.environ.get("AUR_PRIVILEGED", "0") == "1" + ssh_cmd = os.environ.get("SSH_ORIGINAL_COMMAND") + ssh_client = os.environ.get("SSH_CLIENT") if not ssh_cmd: - die_with_help("Interactive shell is disabled.") + die_with_help(f"Welcome to AUR, {user}! Interactive shell is disabled.") cmdargv = shlex.split(ssh_cmd) action = cmdargv[0] - remote_addr = ssh_client.split(' ')[0] if ssh_client else None + remote_addr = ssh_client.split(" ")[0] if ssh_client else None try: serve(action, cmdargv, user, privileged, remote_addr) @@ -607,10 +646,10 @@ def main(): except aurweb.exceptions.BannedException: die("The SSH interface is disabled for your IP address.") except aurweb.exceptions.InvalidArgumentsException as e: - die_with_help('{:s}: {}'.format(action, e)) + die_with_help("{:s}: {}".format(action, e)) except aurweb.exceptions.AurwebException as e: - die('{:s}: {}'.format(action, e)) + die("{:s}: {}".format(action, e)) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/aurweb/git/update.py b/aurweb/git/update.py index 2424bf6c..1118340d 100755 --- a/aurweb/git/update.py +++ b/aurweb/git/update.py @@ -13,23 +13,23 @@ import srcinfo.utils import aurweb.config import aurweb.db -notify_cmd = aurweb.config.get('notifications', 'notify-cmd') +notify_cmd = aurweb.config.get("notifications", "notify-cmd") -repo_path = aurweb.config.get('serve', 'repo-path') -repo_regex = aurweb.config.get('serve', 'repo-regex') +repo_path = aurweb.config.get("serve", "repo-path") +repo_regex = aurweb.config.get("serve", "repo-regex") -max_blob_size = aurweb.config.getint('update', 'max-blob-size') +max_blob_size = aurweb.config.getint("update", "max-blob-size") def size_humanize(num): - for unit in ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB']: + for unit in ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB"]: if abs(num) < 2048.0: if isinstance(num, int): return "{}{}".format(num, unit) else: return "{:.2f}{}".format(num, unit) num /= 1024.0 - return "{:.2f}{}".format(num, 'YiB') + return "{:.2f}{}".format(num, "YiB") def extract_arch_fields(pkginfo, field): @@ -39,20 +39,20 @@ def extract_arch_fields(pkginfo, field): for val in pkginfo[field]: values.append({"value": val, "arch": None}) - for arch in pkginfo['arch']: - if field + '_' + arch in pkginfo: - for val in pkginfo[field + '_' + arch]: + for arch in pkginfo["arch"]: + if field + "_" + arch in pkginfo: + for val in pkginfo[field + "_" + arch]: values.append({"value": val, "arch": arch}) return values def parse_dep(depstring): - dep, _, desc = depstring.partition(': ') - depname = re.sub(r'(<|=|>).*', '', dep) - depcond = dep[len(depname):] + dep, _, desc = depstring.partition(": ") + depname = re.sub(r"(<|=|>).*", "", dep) + depcond = dep[len(depname) :] - return (depname, desc, depcond) + return depname, desc, depcond def create_pkgbase(conn, pkgbase, user): @@ -60,15 +60,18 @@ def create_pkgbase(conn, pkgbase, user): userid = cur.fetchone()[0] now = int(time.time()) - cur = conn.execute("INSERT INTO PackageBases (Name, SubmittedTS, " + - "ModifiedTS, SubmitterUID, MaintainerUID, " + - "FlaggerComment) VALUES (?, ?, ?, ?, ?, '')", - [pkgbase, now, now, userid, userid]) + cur = conn.execute( + "INSERT INTO PackageBases (Name, SubmittedTS, " + + "ModifiedTS, SubmitterUID, MaintainerUID, " + + "FlaggerComment) VALUES (?, ?, ?, ?, ?, '')", + [pkgbase, now, now, userid, userid], + ) pkgbase_id = cur.lastrowid - cur = conn.execute("INSERT INTO PackageNotifications " + - "(PackageBaseID, UserID) VALUES (?, ?)", - [pkgbase_id, userid]) + cur = conn.execute( + "INSERT INTO PackageNotifications " + "(PackageBaseID, UserID) VALUES (?, ?)", + [pkgbase_id, userid], + ) conn.commit() @@ -77,9 +80,10 @@ def create_pkgbase(conn, pkgbase, user): def save_metadata(metadata, conn, user): # noqa: C901 # Obtain package base ID and previous maintainer. - pkgbase = metadata['pkgbase'] - cur = conn.execute("SELECT ID, MaintainerUID FROM PackageBases " - "WHERE Name = ?", [pkgbase]) + pkgbase = metadata["pkgbase"] + cur = conn.execute( + "SELECT ID, MaintainerUID FROM PackageBases " "WHERE Name = ?", [pkgbase] + ) (pkgbase_id, maintainer_uid) = cur.fetchone() was_orphan = not maintainer_uid @@ -89,119 +93,142 @@ def save_metadata(metadata, conn, user): # noqa: C901 # Update package base details and delete current packages. now = int(time.time()) - conn.execute("UPDATE PackageBases SET ModifiedTS = ?, " + - "PackagerUID = ?, OutOfDateTS = NULL WHERE ID = ?", - [now, user_id, pkgbase_id]) - conn.execute("UPDATE PackageBases SET MaintainerUID = ? " + - "WHERE ID = ? AND MaintainerUID IS NULL", - [user_id, pkgbase_id]) - for table in ('Sources', 'Depends', 'Relations', 'Licenses', 'Groups'): - conn.execute("DELETE FROM Package" + table + " WHERE EXISTS (" + - "SELECT * FROM Packages " + - "WHERE Packages.PackageBaseID = ? AND " + - "Package" + table + ".PackageID = Packages.ID)", - [pkgbase_id]) + conn.execute( + "UPDATE PackageBases SET ModifiedTS = ?, " + + "PackagerUID = ?, OutOfDateTS = NULL WHERE ID = ?", + [now, user_id, pkgbase_id], + ) + conn.execute( + "UPDATE PackageBases SET MaintainerUID = ? " + + "WHERE ID = ? AND MaintainerUID IS NULL", + [user_id, pkgbase_id], + ) + for table in ("Sources", "Depends", "Relations", "Licenses", "Groups"): + conn.execute( + "DELETE FROM Package" + + table + + " WHERE EXISTS (" + + "SELECT * FROM Packages " + + "WHERE Packages.PackageBaseID = ? AND " + + "Package" + + table + + ".PackageID = Packages.ID)", + [pkgbase_id], + ) conn.execute("DELETE FROM Packages WHERE PackageBaseID = ?", [pkgbase_id]) for pkgname in srcinfo.utils.get_package_names(metadata): pkginfo = srcinfo.utils.get_merged_package(pkgname, metadata) - if 'epoch' in pkginfo and int(pkginfo['epoch']) > 0: - ver = '{:d}:{:s}-{:s}'.format(int(pkginfo['epoch']), - pkginfo['pkgver'], - pkginfo['pkgrel']) + if "epoch" in pkginfo and int(pkginfo["epoch"]) > 0: + ver = "{:d}:{:s}-{:s}".format( + int(pkginfo["epoch"]), pkginfo["pkgver"], pkginfo["pkgrel"] + ) else: - ver = '{:s}-{:s}'.format(pkginfo['pkgver'], pkginfo['pkgrel']) + ver = "{:s}-{:s}".format(pkginfo["pkgver"], pkginfo["pkgrel"]) - for field in ('pkgdesc', 'url'): + for field in ("pkgdesc", "url"): if field not in pkginfo: pkginfo[field] = None # Create a new package. - cur = conn.execute("INSERT INTO Packages (PackageBaseID, Name, " + - "Version, Description, URL) " + - "VALUES (?, ?, ?, ?, ?)", - [pkgbase_id, pkginfo['pkgname'], ver, - pkginfo['pkgdesc'], pkginfo['url']]) + cur = conn.execute( + "INSERT INTO Packages (PackageBaseID, Name, " + + "Version, Description, URL) " + + "VALUES (?, ?, ?, ?, ?)", + [pkgbase_id, pkginfo["pkgname"], ver, pkginfo["pkgdesc"], pkginfo["url"]], + ) conn.commit() pkgid = cur.lastrowid # Add package sources. - for source_info in extract_arch_fields(pkginfo, 'source'): - conn.execute("INSERT INTO PackageSources (PackageID, Source, " + - "SourceArch) VALUES (?, ?, ?)", - [pkgid, source_info['value'], source_info['arch']]) + for source_info in extract_arch_fields(pkginfo, "source"): + conn.execute( + "INSERT INTO PackageSources (PackageID, Source, " + + "SourceArch) VALUES (?, ?, ?)", + [pkgid, source_info["value"], source_info["arch"]], + ) # Add package dependencies. - for deptype in ('depends', 'makedepends', - 'checkdepends', 'optdepends'): - cur = conn.execute("SELECT ID FROM DependencyTypes WHERE Name = ?", - [deptype]) + for deptype in ("depends", "makedepends", "checkdepends", "optdepends"): + cur = conn.execute( + "SELECT ID FROM DependencyTypes WHERE Name = ?", [deptype] + ) deptypeid = cur.fetchone()[0] for dep_info in extract_arch_fields(pkginfo, deptype): - depname, depdesc, depcond = parse_dep(dep_info['value']) - deparch = dep_info['arch'] - conn.execute("INSERT INTO PackageDepends (PackageID, " + - "DepTypeID, DepName, DepDesc, DepCondition, " + - "DepArch) VALUES (?, ?, ?, ?, ?, ?)", - [pkgid, deptypeid, depname, depdesc, depcond, - deparch]) + depname, depdesc, depcond = parse_dep(dep_info["value"]) + deparch = dep_info["arch"] + conn.execute( + "INSERT INTO PackageDepends (PackageID, " + + "DepTypeID, DepName, DepDesc, DepCondition, " + + "DepArch) VALUES (?, ?, ?, ?, ?, ?)", + [pkgid, deptypeid, depname, depdesc, depcond, deparch], + ) # Add package relations (conflicts, provides, replaces). - for reltype in ('conflicts', 'provides', 'replaces'): - cur = conn.execute("SELECT ID FROM RelationTypes WHERE Name = ?", - [reltype]) + for reltype in ("conflicts", "provides", "replaces"): + cur = conn.execute("SELECT ID FROM RelationTypes WHERE Name = ?", [reltype]) reltypeid = cur.fetchone()[0] for rel_info in extract_arch_fields(pkginfo, reltype): - relname, _, relcond = parse_dep(rel_info['value']) - relarch = rel_info['arch'] - conn.execute("INSERT INTO PackageRelations (PackageID, " + - "RelTypeID, RelName, RelCondition, RelArch) " + - "VALUES (?, ?, ?, ?, ?)", - [pkgid, reltypeid, relname, relcond, relarch]) + relname, _, relcond = parse_dep(rel_info["value"]) + relarch = rel_info["arch"] + conn.execute( + "INSERT INTO PackageRelations (PackageID, " + + "RelTypeID, RelName, RelCondition, RelArch) " + + "VALUES (?, ?, ?, ?, ?)", + [pkgid, reltypeid, relname, relcond, relarch], + ) # Add package licenses. - if 'license' in pkginfo: - for license in pkginfo['license']: - cur = conn.execute("SELECT ID FROM Licenses WHERE Name = ?", - [license]) + if "license" in pkginfo: + for license in pkginfo["license"]: + cur = conn.execute("SELECT ID FROM Licenses WHERE Name = ?", [license]) row = cur.fetchone() if row: licenseid = row[0] else: - cur = conn.execute("INSERT INTO Licenses (Name) " + - "VALUES (?)", [license]) + cur = conn.execute( + "INSERT INTO Licenses (Name) " + "VALUES (?)", [license] + ) conn.commit() licenseid = cur.lastrowid - conn.execute("INSERT INTO PackageLicenses (PackageID, " + - "LicenseID) VALUES (?, ?)", - [pkgid, licenseid]) + conn.execute( + "INSERT INTO PackageLicenses (PackageID, " + + "LicenseID) VALUES (?, ?)", + [pkgid, licenseid], + ) # Add package groups. - if 'groups' in pkginfo: - for group in pkginfo['groups']: - cur = conn.execute("SELECT ID FROM `Groups` WHERE Name = ?", - [group]) + if "groups" in pkginfo: + for group in pkginfo["groups"]: + cur = conn.execute("SELECT ID FROM `Groups` WHERE Name = ?", [group]) row = cur.fetchone() if row: groupid = row[0] else: - cur = conn.execute("INSERT INTO `Groups` (Name) VALUES (?)", - [group]) + cur = conn.execute( + "INSERT INTO `Groups` (Name) VALUES (?)", [group] + ) conn.commit() groupid = cur.lastrowid - conn.execute("INSERT INTO PackageGroups (PackageID, " - "GroupID) VALUES (?, ?)", [pkgid, groupid]) + conn.execute( + "INSERT INTO PackageGroups (PackageID, " "GroupID) VALUES (?, ?)", + [pkgid, groupid], + ) # Add user to notification list on adoption. if was_orphan: - cur = conn.execute("SELECT COUNT(*) FROM PackageNotifications WHERE " + - "PackageBaseID = ? AND UserID = ?", - [pkgbase_id, user_id]) + cur = conn.execute( + "SELECT COUNT(*) FROM PackageNotifications WHERE " + + "PackageBaseID = ? AND UserID = ?", + [pkgbase_id, user_id], + ) if cur.fetchone()[0] == 0: - conn.execute("INSERT INTO PackageNotifications " + - "(PackageBaseID, UserID) VALUES (?, ?)", - [pkgbase_id, user_id]) + conn.execute( + "INSERT INTO PackageNotifications " + + "(PackageBaseID, UserID) VALUES (?, ?)", + [pkgbase_id, user_id], + ) conn.commit() @@ -212,7 +239,7 @@ def update_notify(conn, user, pkgbase_id): user_id = int(cur.fetchone()[0]) # Execute the notification script. - subprocess.Popen((notify_cmd, 'update', str(user_id), str(pkgbase_id))) + subprocess.Popen((notify_cmd, "update", str(user_id), str(pkgbase_id))) def die(msg): @@ -225,28 +252,91 @@ def warn(msg): def die_commit(msg, commit): - sys.stderr.write("error: The following error " + - "occurred when parsing commit\n") + sys.stderr.write("error: The following error " + "occurred when parsing commit\n") sys.stderr.write("error: {:s}:\n".format(commit)) sys.stderr.write("error: {:s}\n".format(msg)) exit(1) +def validate_metadata(metadata, commit): # noqa: C901 + try: + metadata_pkgbase = metadata["pkgbase"] + except KeyError: + die_commit( + "invalid .SRCINFO, does not contain a pkgbase (is the file empty?)", + str(commit.id), + ) + if not re.match(repo_regex, metadata_pkgbase): + die_commit("invalid pkgbase: {:s}".format(metadata_pkgbase), str(commit.id)) + + if not metadata["packages"]: + die_commit("missing pkgname entry", str(commit.id)) + + for pkgname in set(metadata["packages"].keys()): + pkginfo = srcinfo.utils.get_merged_package(pkgname, metadata) + + for field in ("pkgver", "pkgrel", "pkgname"): + if field not in pkginfo: + die_commit( + "missing mandatory field: {:s}".format(field), str(commit.id) + ) + + if "epoch" in pkginfo and not pkginfo["epoch"].isdigit(): + die_commit("invalid epoch: {:s}".format(pkginfo["epoch"]), str(commit.id)) + + if not re.match(r"[a-z0-9][a-z0-9\.+_-]*$", pkginfo["pkgname"]): + die_commit( + "invalid package name: {:s}".format(pkginfo["pkgname"]), + str(commit.id), + ) + + max_len = {"pkgname": 255, "pkgdesc": 255, "url": 8000} + for field in max_len.keys(): + if field in pkginfo and len(pkginfo[field]) > max_len[field]: + die_commit( + "{:s} field too long: {:s}".format(field, pkginfo[field]), + str(commit.id), + ) + + for field in ("install", "changelog"): + if field in pkginfo and not pkginfo[field] in commit.tree: + die_commit( + "missing {:s} file: {:s}".format(field, pkginfo[field]), + str(commit.id), + ) + + for field in extract_arch_fields(pkginfo, "source"): + fname = field["value"] + if len(fname) > 8000: + die_commit("source entry too long: {:s}".format(fname), str(commit.id)) + if "://" in fname or "lp:" in fname: + continue + if fname not in commit.tree: + die_commit("missing source file: {:s}".format(fname), str(commit.id)) + + +def validate_blob_size(blob: pygit2.Object, commit: pygit2.Commit): + if isinstance(blob, pygit2.Blob) and blob.size > max_blob_size: + die_commit( + "maximum blob size ({:s}) exceeded".format(size_humanize(max_blob_size)), + str(commit.id), + ) + + def main(): # noqa: C901 repo = pygit2.Repository(repo_path) user = os.environ.get("AUR_USER") pkgbase = os.environ.get("AUR_PKGBASE") - privileged = (os.environ.get("AUR_PRIVILEGED", '0') == '1') - allow_overwrite = (os.environ.get("AUR_OVERWRITE", '0') == '1') and privileged + privileged = os.environ.get("AUR_PRIVILEGED", "0") == "1" + allow_overwrite = (os.environ.get("AUR_OVERWRITE", "0") == "1") and privileged warn_or_die = warn if privileged else die if len(sys.argv) == 2 and sys.argv[1] == "restore": - if 'refs/heads/' + pkgbase not in repo.listall_references(): - die('{:s}: repository not found: {:s}'.format(sys.argv[1], - pkgbase)) + if "refs/heads/" + pkgbase not in repo.listall_references(): + die("{:s}: repository not found: {:s}".format(sys.argv[1], pkgbase)) refname = "refs/heads/master" - branchref = 'refs/heads/' + pkgbase + branchref = "refs/heads/" + pkgbase sha1_old = sha1_new = repo.lookup_reference(branchref).target elif len(sys.argv) == 4: refname, sha1_old, sha1_new = sys.argv[1:4] @@ -266,137 +356,115 @@ def main(): # noqa: C901 die("denying non-fast-forward (you should pull first)") # Prepare the walker that validates new commits. - walker = repo.walk(sha1_new, pygit2.GIT_SORT_TOPOLOGICAL) + walker = repo.walk(sha1_new, pygit2.GIT_SORT_REVERSE) if sha1_old != "0" * 40: walker.hide(sha1_old) + head_commit = repo[sha1_new] + if ".SRCINFO" not in head_commit.tree: + die_commit("missing .SRCINFO", str(head_commit.id)) + + # Read .SRCINFO from the HEAD commit. + metadata_raw = repo[head_commit.tree[".SRCINFO"].id].data.decode() + (metadata, errors) = srcinfo.parse.parse_srcinfo(metadata_raw) + if errors: + sys.stderr.write( + "error: The following errors occurred " "when parsing .SRCINFO in commit\n" + ) + sys.stderr.write("error: {:s}:\n".format(str(head_commit.id))) + for error in errors: + for err in error["error"]: + sys.stderr.write("error: line {:d}: {:s}\n".format(error["line"], err)) + exit(1) + + # check if there is a correct .SRCINFO file in the latest revision + validate_metadata(metadata, head_commit) + # Validate all new commits. for commit in walker: - for fname in ('.SRCINFO', 'PKGBUILD'): - if fname not in commit.tree: - die_commit("missing {:s}".format(fname), str(commit.id)) + if "PKGBUILD" not in commit.tree: + die_commit("missing PKGBUILD", str(commit.id)) + # Iterate over files in root dir for treeobj in commit.tree: - blob = repo[treeobj.id] + # Don't allow any subdirs besides "keys/" + if isinstance(treeobj, pygit2.Tree) and treeobj.name != "keys": + die_commit( + "the repository must not contain subdirectories", + str(commit.id), + ) - if isinstance(blob, pygit2.Tree): - die_commit("the repository must not contain subdirectories", - str(commit.id)) + # Check size of files in root dir + validate_blob_size(treeobj, commit) - if not isinstance(blob, pygit2.Blob): - die_commit("not a blob object: {:s}".format(treeobj), - str(commit.id)) - - if blob.size > max_blob_size: - die_commit("maximum blob size ({:s}) exceeded".format( - size_humanize(max_blob_size)), str(commit.id)) - - metadata_raw = repo[commit.tree['.SRCINFO'].id].data.decode() - (metadata, errors) = srcinfo.parse.parse_srcinfo(metadata_raw) - if errors: - sys.stderr.write("error: The following errors occurred " - "when parsing .SRCINFO in commit\n") - sys.stderr.write("error: {:s}:\n".format(str(commit.id))) - for error in errors: - for err in error['error']: - sys.stderr.write("error: line {:d}: {:s}\n".format( - error['line'], err)) - exit(1) - - try: - metadata_pkgbase = metadata['pkgbase'] - except KeyError: - die_commit('invalid .SRCINFO, does not contain a pkgbase (is the file empty?)', - str(commit.id)) - if not re.match(repo_regex, metadata_pkgbase): - die_commit('invalid pkgbase: {:s}'.format(metadata_pkgbase), - str(commit.id)) - - if not metadata['packages']: - die_commit('missing pkgname entry', str(commit.id)) - - for pkgname in set(metadata['packages'].keys()): - pkginfo = srcinfo.utils.get_merged_package(pkgname, metadata) - - for field in ('pkgver', 'pkgrel', 'pkgname'): - if field not in pkginfo: - die_commit('missing mandatory field: {:s}'.format(field), - str(commit.id)) - - if 'epoch' in pkginfo and not pkginfo['epoch'].isdigit(): - die_commit('invalid epoch: {:s}'.format(pkginfo['epoch']), - str(commit.id)) - - if not re.match(r'[a-z0-9][a-z0-9\.+_-]*$', pkginfo['pkgname']): - die_commit('invalid package name: {:s}'.format( - pkginfo['pkgname']), str(commit.id)) - - max_len = {'pkgname': 255, 'pkgdesc': 255, 'url': 8000} - for field in max_len.keys(): - if field in pkginfo and len(pkginfo[field]) > max_len[field]: - die_commit('{:s} field too long: {:s}'.format(field, - pkginfo[field]), str(commit.id)) - - for field in ('install', 'changelog'): - if field in pkginfo and not pkginfo[field] in commit.tree: - die_commit('missing {:s} file: {:s}'.format(field, - pkginfo[field]), str(commit.id)) - - for field in extract_arch_fields(pkginfo, 'source'): - fname = field['value'] - if len(fname) > 8000: - die_commit('source entry too long: {:s}'.format(fname), - str(commit.id)) - if "://" in fname or "lp:" in fname: - continue - if fname not in commit.tree: - die_commit('missing source file: {:s}'.format(fname), - str(commit.id)) + # If we got a subdir keys/, + # make sure it only contains a pgp/ subdir with key files + if "keys" in commit.tree: + # Check for forbidden files/dirs in keys/ + for keyobj in commit.tree["keys"]: + if not isinstance(keyobj, pygit2.Tree) or keyobj.name != "pgp": + die_commit( + "the keys/ subdir may only contain a pgp/ directory", + str(commit.id), + ) + # Check for forbidden files in keys/pgp/ + if "keys/pgp" in commit.tree: + for pgpobj in commit.tree["keys/pgp"]: + if not isinstance(pgpobj, pygit2.Blob) or not pgpobj.name.endswith( + ".asc" + ): + die_commit( + "the subdir may only contain .asc (PGP pub key) files", + str(commit.id), + ) + # Check file size for pgp key files + validate_blob_size(pgpobj, commit) # Display a warning if .SRCINFO is unchanged. if sha1_old not in ("0000000000000000000000000000000000000000", sha1_new): - srcinfo_id_old = repo[sha1_old].tree['.SRCINFO'].id - srcinfo_id_new = repo[sha1_new].tree['.SRCINFO'].id + srcinfo_id_old = repo[sha1_old].tree[".SRCINFO"].id + srcinfo_id_new = repo[sha1_new].tree[".SRCINFO"].id if srcinfo_id_old == srcinfo_id_new: - warn(".SRCINFO unchanged. " - "The package database will not be updated!") - - # Read .SRCINFO from the HEAD commit. - metadata_raw = repo[repo[sha1_new].tree['.SRCINFO'].id].data.decode() - (metadata, errors) = srcinfo.parse.parse_srcinfo(metadata_raw) + warn(".SRCINFO unchanged. " "The package database will not be updated!") # Ensure that the package base name matches the repository name. - metadata_pkgbase = metadata['pkgbase'] + metadata_pkgbase = metadata["pkgbase"] if metadata_pkgbase != pkgbase: - die('invalid pkgbase: {:s}, expected {:s}'.format(metadata_pkgbase, - pkgbase)) + die("invalid pkgbase: {:s}, expected {:s}".format(metadata_pkgbase, pkgbase)) # Ensure that packages are neither blacklisted nor overwritten. - pkgbase = metadata['pkgbase'] + pkgbase = metadata["pkgbase"] cur = conn.execute("SELECT ID FROM PackageBases WHERE Name = ?", [pkgbase]) row = cur.fetchone() pkgbase_id = row[0] if row else 0 cur = conn.execute("SELECT Name FROM PackageBlacklist") blacklist = [row[0] for row in cur.fetchall()] + if pkgbase in blacklist: + warn_or_die("pkgbase is blacklisted: {:s}".format(pkgbase)) cur = conn.execute("SELECT Name, Repo FROM OfficialProviders") providers = dict(cur.fetchall()) for pkgname in srcinfo.utils.get_package_names(metadata): pkginfo = srcinfo.utils.get_merged_package(pkgname, metadata) - pkgname = pkginfo['pkgname'] + pkgname = pkginfo["pkgname"] if pkgname in blacklist: - warn_or_die('package is blacklisted: {:s}'.format(pkgname)) + warn_or_die("package is blacklisted: {:s}".format(pkgname)) if pkgname in providers: - warn_or_die('package already provided by [{:s}]: {:s}'.format( - providers[pkgname], pkgname)) + warn_or_die( + "package already provided by [{:s}]: {:s}".format( + providers[pkgname], pkgname + ) + ) - cur = conn.execute("SELECT COUNT(*) FROM Packages WHERE Name = ? " + - "AND PackageBaseID <> ?", [pkgname, pkgbase_id]) + cur = conn.execute( + "SELECT COUNT(*) FROM Packages WHERE Name = ? " + "AND PackageBaseID <> ?", + [pkgname, pkgbase_id], + ) if cur.fetchone()[0] > 0: - die('cannot overwrite package: {:s}'.format(pkgname)) + die("cannot overwrite package: {:s}".format(pkgname)) # Create a new package base if it does not exist yet. if pkgbase_id == 0: @@ -407,7 +475,7 @@ def main(): # noqa: C901 # Create (or update) a branch with the name of the package base for better # accessibility. - branchref = 'refs/heads/' + pkgbase + branchref = "refs/heads/" + pkgbase repo.create_reference(branchref, sha1_new, True) # Work around a Git bug: The HEAD ref is not updated when using @@ -415,7 +483,7 @@ def main(): # noqa: C901 # mainline. See # http://git.661346.n2.nabble.com/PATCH-receive-pack-Create-a-HEAD-ref-for-ref-namespace-td7632149.html # for details. - headref = 'refs/namespaces/' + pkgbase + '/HEAD' + headref = "refs/namespaces/" + pkgbase + "/HEAD" repo.create_reference(headref, sha1_new, True) # Send package update notifications. @@ -426,5 +494,5 @@ def main(): # noqa: C901 conn.close() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/aurweb/initdb.py b/aurweb/initdb.py index a4a9f621..7181ea3e 100644 --- a/aurweb/initdb.py +++ b/aurweb/initdb.py @@ -3,34 +3,46 @@ import argparse import alembic.command import alembic.config +import aurweb.aur_logging import aurweb.db -import aurweb.logging import aurweb.schema def feed_initial_data(conn): - conn.execute(aurweb.schema.AccountTypes.insert(), [ - {'ID': 1, 'AccountType': 'User'}, - {'ID': 2, 'AccountType': 'Trusted User'}, - {'ID': 3, 'AccountType': 'Developer'}, - {'ID': 4, 'AccountType': 'Trusted User & Developer'}, - ]) - conn.execute(aurweb.schema.DependencyTypes.insert(), [ - {'ID': 1, 'Name': 'depends'}, - {'ID': 2, 'Name': 'makedepends'}, - {'ID': 3, 'Name': 'checkdepends'}, - {'ID': 4, 'Name': 'optdepends'}, - ]) - conn.execute(aurweb.schema.RelationTypes.insert(), [ - {'ID': 1, 'Name': 'conflicts'}, - {'ID': 2, 'Name': 'provides'}, - {'ID': 3, 'Name': 'replaces'}, - ]) - conn.execute(aurweb.schema.RequestTypes.insert(), [ - {'ID': 1, 'Name': 'deletion'}, - {'ID': 2, 'Name': 'orphan'}, - {'ID': 3, 'Name': 'merge'}, - ]) + conn.execute( + aurweb.schema.AccountTypes.insert(), + [ + {"ID": 1, "AccountType": "User"}, + {"ID": 2, "AccountType": "Package Maintainer"}, + {"ID": 3, "AccountType": "Developer"}, + {"ID": 4, "AccountType": "Package Maintainer & Developer"}, + ], + ) + conn.execute( + aurweb.schema.DependencyTypes.insert(), + [ + {"ID": 1, "Name": "depends"}, + {"ID": 2, "Name": "makedepends"}, + {"ID": 3, "Name": "checkdepends"}, + {"ID": 4, "Name": "optdepends"}, + ], + ) + conn.execute( + aurweb.schema.RelationTypes.insert(), + [ + {"ID": 1, "Name": "conflicts"}, + {"ID": 2, "Name": "provides"}, + {"ID": 3, "Name": "replaces"}, + ], + ) + conn.execute( + aurweb.schema.RequestTypes.insert(), + [ + {"ID": 1, "Name": "deletion"}, + {"ID": 2, "Name": "orphan"}, + {"ID": 3, "Name": "merge"}, + ], + ) def run(args): @@ -40,8 +52,8 @@ def run(args): # the last step and leave the database in an inconsistent state. The # configuration is loaded lazily, so we query it to force its loading. if args.use_alembic: - alembic_config = alembic.config.Config('alembic.ini') - alembic_config.get_main_option('script_location') + alembic_config = alembic.config.Config("alembic.ini") + alembic_config.get_main_option("script_location") alembic_config.attributes["configure_logger"] = False engine = aurweb.db.get_engine(echo=(args.verbose >= 1)) @@ -51,17 +63,21 @@ def run(args): conn.close() if args.use_alembic: - alembic.command.stamp(alembic_config, 'head') + alembic.command.stamp(alembic_config, "head") -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser( - prog='python -m aurweb.initdb', - description='Initialize the aurweb database.') - parser.add_argument('-v', '--verbose', action='count', default=0, - help='increase verbosity') - parser.add_argument('--no-alembic', - help='disable Alembic migrations support', - dest='use_alembic', action='store_false') + prog="python -m aurweb.initdb", description="Initialize the aurweb database." + ) + parser.add_argument( + "-v", "--verbose", action="count", default=0, help="increase verbosity" + ) + parser.add_argument( + "--no-alembic", + help="disable Alembic migrations support", + dest="use_alembic", + action="store_false", + ) args = parser.parse_args() run(args) diff --git a/aurweb/l10n.py b/aurweb/l10n.py index 4998d8ee..1ce5e956 100644 --- a/aurweb/l10n.py +++ b/aurweb/l10n.py @@ -1,43 +1,44 @@ import gettext - from collections import OrderedDict from fastapi import Request import aurweb.config -SUPPORTED_LANGUAGES = OrderedDict({ - "ar": "العربية", - "ast": "Asturianu", - "ca": "Català", - "cs": "Český", - "da": "Dansk", - "de": "Deutsch", - "el": "Ελληνικά", - "en": "English", - "es": "Español", - "es_419": "Español (Latinoamérica)", - "fi": "Suomi", - "fr": "Français", - "he": "עברית", - "hr": "Hrvatski", - "hu": "Magyar", - "it": "Italiano", - "ja": "日本語", - "nb": "Norsk", - "nl": "Nederlands", - "pl": "Polski", - "pt_BR": "Português (Brasil)", - "pt_PT": "Português (Portugal)", - "ro": "Română", - "ru": "Русский", - "sk": "Slovenčina", - "sr": "Srpski", - "tr": "Türkçe", - "uk": "Українська", - "zh_CN": "简体中文", - "zh_TW": "正體中文" -}) +SUPPORTED_LANGUAGES = OrderedDict( + { + "ar": "العربية", + "ast": "Asturianu", + "ca": "Català", + "cs": "Český", + "da": "Dansk", + "de": "Deutsch", + "el": "Ελληνικά", + "en": "English", + "es": "Español", + "es_419": "Español (Latinoamérica)", + "fi": "Suomi", + "fr": "Français", + "he": "עברית", + "hr": "Hrvatski", + "hu": "Magyar", + "it": "Italiano", + "ja": "日本語", + "nb": "Norsk", + "nl": "Nederlands", + "pl": "Polski", + "pt_BR": "Português (Brasil)", + "pt_PT": "Português (Portugal)", + "ro": "Română", + "ru": "Русский", + "sk": "Slovenčina", + "sr": "Srpski", + "tr": "Türkçe", + "uk": "Українська", + "zh_CN": "简体中文", + "zh_TW": "正體中文", + } +) RIGHT_TO_LEFT_LANGUAGES = ("he", "ar") @@ -45,15 +46,14 @@ RIGHT_TO_LEFT_LANGUAGES = ("he", "ar") class Translator: def __init__(self): - self._localedir = aurweb.config.get('options', 'localedir') + self._localedir = aurweb.config.get("options", "localedir") self._translator = {} def get_translator(self, lang: str): if lang not in self._translator: - self._translator[lang] = gettext.translation("aurweb", - self._localedir, - languages=[lang], - fallback=True) + self._translator[lang] = gettext.translation( + "aurweb", self._localedir, languages=[lang], fallback=True + ) return self._translator.get(lang) def translate(self, s: str, lang: str): @@ -64,11 +64,24 @@ class Translator: translator = Translator() -def get_request_language(request: Request): - if request.user.is_authenticated(): +def get_request_language(request: Request) -> str: + """Get a request's language from either query param, user setting or + cookie. We use the configuration's [options] default_lang otherwise. + + @param request FastAPI request + """ + request_lang = request.query_params.get("language") + cookie_lang = request.cookies.get("AURLANG") + if request_lang and request_lang in SUPPORTED_LANGUAGES: + return request_lang + elif ( + request.user.is_authenticated() + and request.user.LangPreference in SUPPORTED_LANGUAGES + ): return request.user.LangPreference - default_lang = aurweb.config.get("options", "default_lang") - return request.cookies.get("AURLANG", default_lang) + elif cookie_lang and cookie_lang in SUPPORTED_LANGUAGES: + return cookie_lang + return aurweb.config.get_with_fallback("options", "default_lang", "en") def get_raw_translator_for_request(request: Request): diff --git a/aurweb/models/__init__.py b/aurweb/models/__init__.py index a06077ad..90f3c93f 100644 --- a/aurweb/models/__init__.py +++ b/aurweb/models/__init__.py @@ -1,4 +1,5 @@ """ Collection of all aurweb SQLAlchemy declarative models. """ + from .accepted_term import AcceptedTerm # noqa: F401 from .account_type import AccountType # noqa: F401 from .api_rate_limit import ApiRateLimit # noqa: F401 @@ -26,6 +27,6 @@ from .request_type import RequestType # noqa: F401 from .session import Session # noqa: F401 from .ssh_pub_key import SSHPubKey # noqa: F401 from .term import Term # noqa: F401 -from .tu_vote import TUVote # noqa: F401 -from .tu_voteinfo import TUVoteInfo # noqa: F401 from .user import User # noqa: F401 +from .vote import Vote # noqa: F401 +from .voteinfo import VoteInfo # noqa: F401 diff --git a/aurweb/models/accepted_term.py b/aurweb/models/accepted_term.py index 0f9b187e..022075e8 100644 --- a/aurweb/models/accepted_term.py +++ b/aurweb/models/accepted_term.py @@ -13,12 +13,16 @@ class AcceptedTerm(Base): __mapper_args__ = {"primary_key": [__table__.c.TermsID]} User = relationship( - _User, backref=backref("accepted_terms", lazy="dynamic"), - foreign_keys=[__table__.c.UsersID]) + _User, + backref=backref("accepted_terms", lazy="dynamic"), + foreign_keys=[__table__.c.UsersID], + ) Term = relationship( - _Term, backref=backref("accepted_terms", lazy="dynamic"), - foreign_keys=[__table__.c.TermsID]) + _Term, + backref=backref("accepted_terms", lazy="dynamic"), + foreign_keys=[__table__.c.TermsID], + ) def __init__(self, **kwargs): super().__init__(**kwargs) @@ -27,10 +31,12 @@ class AcceptedTerm(Base): raise IntegrityError( statement="Foreign key UsersID cannot be null.", orig="AcceptedTerms.UserID", - params=("NULL")) + params=("NULL"), + ) if not self.Term and not self.TermsID: raise IntegrityError( statement="Foreign key TermID cannot be null.", orig="AcceptedTerms.TermID", - params=("NULL")) + params=("NULL"), + ) diff --git a/aurweb/models/account_type.py b/aurweb/models/account_type.py index a849df02..70bfc2c5 100644 --- a/aurweb/models/account_type.py +++ b/aurweb/models/account_type.py @@ -2,21 +2,21 @@ from aurweb import schema from aurweb.models.declarative import Base USER = "User" -TRUSTED_USER = "Trusted User" +PACKAGE_MAINTAINER = "Package Maintainer" DEVELOPER = "Developer" -TRUSTED_USER_AND_DEV = "Trusted User & Developer" +PACKAGE_MAINTAINER_AND_DEV = "Package Maintainer & Developer" USER_ID = 1 -TRUSTED_USER_ID = 2 +PACKAGE_MAINTAINER_ID = 2 DEVELOPER_ID = 3 -TRUSTED_USER_AND_DEV_ID = 4 +PACKAGE_MAINTAINER_AND_DEV_ID = 4 # Map string constants to integer constants. ACCOUNT_TYPE_ID = { USER: USER_ID, - TRUSTED_USER: TRUSTED_USER_ID, + PACKAGE_MAINTAINER: PACKAGE_MAINTAINER_ID, DEVELOPER: DEVELOPER_ID, - TRUSTED_USER_AND_DEV: TRUSTED_USER_AND_DEV_ID + PACKAGE_MAINTAINER_AND_DEV: PACKAGE_MAINTAINER_AND_DEV_ID, } # Reversed ACCOUNT_TYPE_ID mapping. @@ -24,7 +24,8 @@ ACCOUNT_TYPE_NAME = {v: k for k, v in ACCOUNT_TYPE_ID.items()} class AccountType(Base): - """ An ORM model of a single AccountTypes record. """ + """An ORM model of a single AccountTypes record.""" + __table__ = schema.AccountTypes __tablename__ = __table__.name __mapper_args__ = {"primary_key": [__table__.c.ID]} @@ -36,5 +37,4 @@ class AccountType(Base): return str(self.AccountType) def __repr__(self): - return "" % ( - self.ID, str(self)) + return "" % (self.ID, str(self)) diff --git a/aurweb/models/api_rate_limit.py b/aurweb/models/api_rate_limit.py index 19b656df..ce195a80 100644 --- a/aurweb/models/api_rate_limit.py +++ b/aurweb/models/api_rate_limit.py @@ -16,10 +16,12 @@ class ApiRateLimit(Base): raise IntegrityError( statement="Column Requests cannot be null.", orig="ApiRateLimit.Requests", - params=("NULL")) + params=("NULL"), + ) if self.WindowStart is None: raise IntegrityError( statement="Column WindowStart cannot be null.", orig="ApiRateLimit.WindowStart", - params=("NULL")) + params=("NULL"), + ) diff --git a/aurweb/models/ban.py b/aurweb/models/ban.py index 0fcb6d2e..d2a7250d 100644 --- a/aurweb/models/ban.py +++ b/aurweb/models/ban.py @@ -2,6 +2,7 @@ from fastapi import Request from aurweb import db, schema from aurweb.models.declarative import Base +from aurweb.util import get_client_ip class Ban(Base): @@ -14,6 +15,6 @@ class Ban(Base): def is_banned(request: Request): - ip = request.client.host + ip = get_client_ip(request) exists = db.query(Ban).filter(Ban.IPAddress == ip).exists() return db.query(exists).scalar() diff --git a/aurweb/models/declarative.py b/aurweb/models/declarative.py index 20ddd20c..22df31c7 100644 --- a/aurweb/models/declarative.py +++ b/aurweb/models/declarative.py @@ -6,26 +6,19 @@ from aurweb import util def to_dict(model): - return { - c.name: getattr(model, c.name) - for c in model.__table__.columns - } + return {c.name: getattr(model, c.name) for c in model.__table__.columns} def to_json(model, indent: int = None): - return json.dumps({ - k: util.jsonify(v) - for k, v in to_dict(model).items() - }, indent=indent) + return json.dumps( + {k: util.jsonify(v) for k, v in to_dict(model).items()}, indent=indent + ) Base = declarative_base() # Setup __table_args__ applicable to every table. -Base.__table_args__ = { - "autoload": False, - "extend_existing": True -} +Base.__table_args__ = {"autoload": False, "extend_existing": True} # Setup Base.as_dict and Base.json. # diff --git a/aurweb/models/group.py b/aurweb/models/group.py index 0275ed94..a6870db6 100644 --- a/aurweb/models/group.py +++ b/aurweb/models/group.py @@ -15,4 +15,5 @@ class Group(Base): raise IntegrityError( statement="Column Name cannot be null.", orig="Groups.Name", - params=("NULL")) + params=("NULL"), + ) diff --git a/aurweb/models/license.py b/aurweb/models/license.py index 86aeaa86..f2b02a87 100644 --- a/aurweb/models/license.py +++ b/aurweb/models/license.py @@ -16,4 +16,5 @@ class License(Base): raise IntegrityError( statement="Column Name cannot be null.", orig="Licenses.Name", - params=("NULL")) + params=("NULL"), + ) diff --git a/aurweb/models/official_provider.py b/aurweb/models/official_provider.py index e111569e..0da9f76c 100644 --- a/aurweb/models/official_provider.py +++ b/aurweb/models/official_provider.py @@ -21,16 +21,19 @@ class OfficialProvider(Base): raise IntegrityError( statement="Column Name cannot be null.", orig="OfficialProviders.Name", - params=("NULL")) + params=("NULL"), + ) if not self.Repo: raise IntegrityError( statement="Column Repo cannot be null.", orig="OfficialProviders.Repo", - params=("NULL")) + params=("NULL"), + ) if not self.Provides: raise IntegrityError( statement="Column Provides cannot be null.", orig="OfficialProviders.Provides", - params=("NULL")) + params=("NULL"), + ) diff --git a/aurweb/models/package.py b/aurweb/models/package.py index 64c6a195..e98029f3 100644 --- a/aurweb/models/package.py +++ b/aurweb/models/package.py @@ -12,9 +12,10 @@ class Package(Base): __mapper_args__ = {"primary_key": [__table__.c.ID]} PackageBase = relationship( - _PackageBase, backref=backref("packages", lazy="dynamic", - cascade="all, delete"), - foreign_keys=[__table__.c.PackageBaseID]) + _PackageBase, + backref=backref("packages", lazy="dynamic", cascade="all, delete"), + foreign_keys=[__table__.c.PackageBaseID], + ) # No Package instances are official packages. is_official = False @@ -26,10 +27,12 @@ class Package(Base): raise IntegrityError( statement="Foreign key PackageBaseID cannot be null.", orig="Packages.PackageBaseID", - params=("NULL")) + params=("NULL"), + ) if self.Name is None: raise IntegrityError( statement="Column Name cannot be null.", orig="Packages.Name", - params=("NULL")) + params=("NULL"), + ) diff --git a/aurweb/models/package_base.py b/aurweb/models/package_base.py index 37ad63ce..26d9165f 100644 --- a/aurweb/models/package_base.py +++ b/aurweb/models/package_base.py @@ -12,20 +12,28 @@ class PackageBase(Base): __mapper_args__ = {"primary_key": [__table__.c.ID]} Flagger = relationship( - _User, backref=backref("flagged_bases", lazy="dynamic"), - foreign_keys=[__table__.c.FlaggerUID]) + _User, + backref=backref("flagged_bases", lazy="dynamic"), + foreign_keys=[__table__.c.FlaggerUID], + ) Submitter = relationship( - _User, backref=backref("submitted_bases", lazy="dynamic"), - foreign_keys=[__table__.c.SubmitterUID]) + _User, + backref=backref("submitted_bases", lazy="dynamic"), + foreign_keys=[__table__.c.SubmitterUID], + ) Maintainer = relationship( - _User, backref=backref("maintained_bases", lazy="dynamic"), - foreign_keys=[__table__.c.MaintainerUID]) + _User, + backref=backref("maintained_bases", lazy="dynamic"), + foreign_keys=[__table__.c.MaintainerUID], + ) Packager = relationship( - _User, backref=backref("package_bases", lazy="dynamic"), - foreign_keys=[__table__.c.PackagerUID]) + _User, + backref=backref("package_bases", lazy="dynamic"), + foreign_keys=[__table__.c.PackagerUID], + ) # A set used to check for floatable values. TO_FLOAT = {"Popularity"} @@ -37,7 +45,8 @@ class PackageBase(Base): raise IntegrityError( statement="Column Name cannot be null.", orig="PackageBases.Name", - params=("NULL")) + params=("NULL"), + ) # If no SubmittedTS/ModifiedTS is provided on creation, set them # here to the current utc timestamp. @@ -55,3 +64,13 @@ class PackageBase(Base): if key in PackageBase.TO_FLOAT and not isinstance(attr, float): return float(attr) return attr + + +def popularity_decay(pkgbase: PackageBase, utcnow: int): + """Return the delta between now and the last time popularity was updated, in days""" + return int((utcnow - pkgbase.PopularityUpdated.timestamp()) / 86400) + + +def popularity(pkgbase: PackageBase, utcnow: int): + """Return up-to-date popularity""" + return float(pkgbase.Popularity) * (0.98 ** popularity_decay(pkgbase, utcnow)) diff --git a/aurweb/models/package_blacklist.py b/aurweb/models/package_blacklist.py index 0f8f0cee..7f6e75ea 100644 --- a/aurweb/models/package_blacklist.py +++ b/aurweb/models/package_blacklist.py @@ -16,4 +16,5 @@ class PackageBlacklist(Base): raise IntegrityError( statement="Column Name cannot be null.", orig="PackageBlacklist.Name", - params=("NULL")) + params=("NULL"), + ) diff --git a/aurweb/models/package_comaintainer.py b/aurweb/models/package_comaintainer.py index b5cdcf38..4bd7f6b3 100644 --- a/aurweb/models/package_comaintainer.py +++ b/aurweb/models/package_comaintainer.py @@ -10,19 +10,19 @@ from aurweb.models.user import User as _User class PackageComaintainer(Base): __table__ = schema.PackageComaintainers __tablename__ = __table__.name - __mapper_args__ = { - "primary_key": [__table__.c.UsersID, __table__.c.PackageBaseID] - } + __mapper_args__ = {"primary_key": [__table__.c.UsersID, __table__.c.PackageBaseID]} User = relationship( - _User, backref=backref("comaintained", lazy="dynamic", - cascade="all, delete"), - foreign_keys=[__table__.c.UsersID]) + _User, + backref=backref("comaintained", lazy="dynamic", cascade="all, delete"), + foreign_keys=[__table__.c.UsersID], + ) PackageBase = relationship( - _PackageBase, backref=backref("comaintainers", lazy="dynamic", - cascade="all, delete"), - foreign_keys=[__table__.c.PackageBaseID]) + _PackageBase, + backref=backref("comaintainers", lazy="dynamic", cascade="all, delete"), + foreign_keys=[__table__.c.PackageBaseID], + ) def __init__(self, **kwargs): super().__init__(**kwargs) @@ -31,16 +31,19 @@ class PackageComaintainer(Base): raise IntegrityError( statement="Foreign key UsersID cannot be null.", orig="PackageComaintainers.UsersID", - params=("NULL")) + params=("NULL"), + ) if not self.PackageBase and not self.PackageBaseID: raise IntegrityError( statement="Foreign key PackageBaseID cannot be null.", orig="PackageComaintainers.PackageBaseID", - params=("NULL")) + params=("NULL"), + ) if not self.Priority: raise IntegrityError( statement="Column Priority cannot be null.", orig="PackageComaintainers.Priority", - params=("NULL")) + params=("NULL"), + ) diff --git a/aurweb/models/package_comment.py b/aurweb/models/package_comment.py index 7ab1f218..64b339a0 100644 --- a/aurweb/models/package_comment.py +++ b/aurweb/models/package_comment.py @@ -13,21 +13,28 @@ class PackageComment(Base): __mapper_args__ = {"primary_key": [__table__.c.ID]} PackageBase = relationship( - _PackageBase, backref=backref("comments", lazy="dynamic", - cascade="all, delete"), - foreign_keys=[__table__.c.PackageBaseID]) + _PackageBase, + backref=backref("comments", lazy="dynamic", cascade="all, delete"), + foreign_keys=[__table__.c.PackageBaseID], + ) User = relationship( - _User, backref=backref("package_comments", lazy="dynamic"), - foreign_keys=[__table__.c.UsersID]) + _User, + backref=backref("package_comments", lazy="dynamic"), + foreign_keys=[__table__.c.UsersID], + ) Editor = relationship( - _User, backref=backref("edited_comments", lazy="dynamic"), - foreign_keys=[__table__.c.EditedUsersID]) + _User, + backref=backref("edited_comments", lazy="dynamic"), + foreign_keys=[__table__.c.EditedUsersID], + ) Deleter = relationship( - _User, backref=backref("deleted_comments", lazy="dynamic"), - foreign_keys=[__table__.c.DelUsersID]) + _User, + backref=backref("deleted_comments", lazy="dynamic"), + foreign_keys=[__table__.c.DelUsersID], + ) def __init__(self, **kwargs): super().__init__(**kwargs) @@ -36,27 +43,31 @@ class PackageComment(Base): raise IntegrityError( statement="Foreign key PackageBaseID cannot be null.", orig="PackageComments.PackageBaseID", - params=("NULL")) + params=("NULL"), + ) if not self.User and not self.UsersID: raise IntegrityError( statement="Foreign key UsersID cannot be null.", orig="PackageComments.UsersID", - params=("NULL")) + params=("NULL"), + ) if self.Comments is None: raise IntegrityError( statement="Column Comments cannot be null.", orig="PackageComments.Comments", - params=("NULL")) + params=("NULL"), + ) if self.RenderedComment is None: self.RenderedComment = str() def maintainers(self): - return list(filter( - lambda e: e is not None, - [self.PackageBase.Maintainer] + [ - c.User for c in self.PackageBase.comaintainers - ] - )) + return list( + filter( + lambda e: e is not None, + [self.PackageBase.Maintainer] + + [c.User for c in self.PackageBase.comaintainers], + ) + ) diff --git a/aurweb/models/package_dependency.py b/aurweb/models/package_dependency.py index 2fd87f2a..9cf1eda0 100644 --- a/aurweb/models/package_dependency.py +++ b/aurweb/models/package_dependency.py @@ -1,5 +1,3 @@ -from typing import List - from sqlalchemy import and_, literal from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship @@ -24,14 +22,16 @@ class PackageDependency(Base): } Package = relationship( - _Package, backref=backref("package_dependencies", lazy="dynamic", - cascade="all, delete"), - foreign_keys=[__table__.c.PackageID]) + _Package, + backref=backref("package_dependencies", lazy="dynamic", cascade="all, delete"), + foreign_keys=[__table__.c.PackageID], + ) DependencyType = relationship( _DependencyType, backref=backref("package_dependencies", lazy="dynamic"), - foreign_keys=[__table__.c.DepTypeID]) + foreign_keys=[__table__.c.DepTypeID], + ) def __init__(self, **kwargs): super().__init__(**kwargs) @@ -40,43 +40,61 @@ class PackageDependency(Base): raise IntegrityError( statement="Foreign key PackageID cannot be null.", orig="PackageDependencies.PackageID", - params=("NULL")) + params=("NULL"), + ) if not self.DependencyType and not self.DepTypeID: raise IntegrityError( statement="Foreign key DepTypeID cannot be null.", orig="PackageDependencies.DepTypeID", - params=("NULL")) + params=("NULL"), + ) if self.DepName is None: raise IntegrityError( statement="Column DepName cannot be null.", orig="PackageDependencies.DepName", - params=("NULL")) + params=("NULL"), + ) + + def is_aur_package(self) -> bool: + pkg = db.query(_Package).filter(_Package.Name == self.DepName).exists() + return db.query(pkg).scalar() def is_package(self) -> bool: - pkg = db.query(_Package).filter(_Package.Name == self.DepName).exists() - official = db.query(_OfficialProvider).filter( - _OfficialProvider.Name == self.DepName).exists() - return db.query(pkg).scalar() or db.query(official).scalar() + official = ( + db.query(_OfficialProvider) + .filter(_OfficialProvider.Name == self.DepName) + .exists() + ) + return self.is_aur_package() or db.query(official).scalar() - def provides(self) -> List[PackageRelation]: + def provides(self) -> list[PackageRelation]: from aurweb.models.relation_type import PROVIDES_ID - rels = db.query(PackageRelation).join(_Package).filter( - and_(PackageRelation.RelTypeID == PROVIDES_ID, - PackageRelation.RelName == self.DepName) - ).with_entities( - _Package.Name, - literal(False).label("is_official") - ).order_by(_Package.Name.asc()) + rels = ( + db.query(PackageRelation) + .join(_Package) + .filter( + and_( + PackageRelation.RelTypeID == PROVIDES_ID, + PackageRelation.RelName == self.DepName, + ) + ) + .with_entities(_Package.Name, literal(False).label("is_official")) + .order_by(_Package.Name.asc()) + ) - official_rels = db.query(_OfficialProvider).filter( - and_(_OfficialProvider.Provides == self.DepName, - _OfficialProvider.Name != self.DepName) - ).with_entities( - _OfficialProvider.Name, - literal(True).label("is_official") - ).order_by(_OfficialProvider.Name.asc()) + official_rels = ( + db.query(_OfficialProvider) + .filter( + and_( + _OfficialProvider.Provides == self.DepName, + _OfficialProvider.Name != self.DepName, + ) + ) + .with_entities(_OfficialProvider.Name, literal(True).label("is_official")) + .order_by(_OfficialProvider.Name.asc()) + ) return rels.union(official_rels).all() diff --git a/aurweb/models/package_group.py b/aurweb/models/package_group.py index dd212051..4e7c55ee 100644 --- a/aurweb/models/package_group.py +++ b/aurweb/models/package_group.py @@ -10,19 +10,19 @@ from aurweb.models.package import Package as _Package class PackageGroup(Base): __table__ = schema.PackageGroups __tablename__ = __table__.name - __mapper_args__ = { - "primary_key": [__table__.c.PackageID, __table__.c.GroupID] - } + __mapper_args__ = {"primary_key": [__table__.c.PackageID, __table__.c.GroupID]} Package = relationship( - _Package, backref=backref("package_groups", lazy="dynamic", - cascade="all, delete"), - foreign_keys=[__table__.c.PackageID]) + _Package, + backref=backref("package_groups", lazy="dynamic", cascade="all, delete"), + foreign_keys=[__table__.c.PackageID], + ) Group = relationship( - _Group, backref=backref("package_groups", lazy="dynamic", - cascade="all, delete"), - foreign_keys=[__table__.c.GroupID]) + _Group, + backref=backref("package_groups", lazy="dynamic", cascade="all, delete"), + foreign_keys=[__table__.c.GroupID], + ) def __init__(self, **kwargs): super().__init__(**kwargs) @@ -31,10 +31,12 @@ class PackageGroup(Base): raise IntegrityError( statement="Primary key PackageID cannot be null.", orig="PackageGroups.PackageID", - params=("NULL")) + params=("NULL"), + ) if not self.Group and not self.GroupID: raise IntegrityError( statement="Primary key GroupID cannot be null.", orig="PackageGroups.GroupID", - params=("NULL")) + params=("NULL"), + ) diff --git a/aurweb/models/package_keyword.py b/aurweb/models/package_keyword.py index 581aafdc..dfacd7c0 100644 --- a/aurweb/models/package_keyword.py +++ b/aurweb/models/package_keyword.py @@ -9,14 +9,13 @@ from aurweb.models.package_base import PackageBase as _PackageBase class PackageKeyword(Base): __table__ = schema.PackageKeywords __tablename__ = __table__.name - __mapper_args__ = { - "primary_key": [__table__.c.PackageBaseID, __table__.c.Keyword] - } + __mapper_args__ = {"primary_key": [__table__.c.PackageBaseID, __table__.c.Keyword]} PackageBase = relationship( - _PackageBase, backref=backref("keywords", lazy="dynamic", - cascade="all, delete"), - foreign_keys=[__table__.c.PackageBaseID]) + _PackageBase, + backref=backref("keywords", lazy="dynamic", cascade="all, delete"), + foreign_keys=[__table__.c.PackageBaseID], + ) def __init__(self, **kwargs): super().__init__(**kwargs) @@ -25,4 +24,5 @@ class PackageKeyword(Base): raise IntegrityError( statement="Primary key PackageBaseID cannot be null.", orig="PackageKeywords.PackageBaseID", - params=("NULL")) + params=("NULL"), + ) diff --git a/aurweb/models/package_license.py b/aurweb/models/package_license.py index 43dd0339..c421defe 100644 --- a/aurweb/models/package_license.py +++ b/aurweb/models/package_license.py @@ -10,19 +10,19 @@ from aurweb.models.package import Package as _Package class PackageLicense(Base): __table__ = schema.PackageLicenses __tablename__ = __table__.name - __mapper_args__ = { - "primary_key": [__table__.c.PackageID, __table__.c.LicenseID] - } + __mapper_args__ = {"primary_key": [__table__.c.PackageID, __table__.c.LicenseID]} Package = relationship( - _Package, backref=backref("package_licenses", lazy="dynamic", - cascade="all, delete"), - foreign_keys=[__table__.c.PackageID]) + _Package, + backref=backref("package_licenses", lazy="dynamic", cascade="all, delete"), + foreign_keys=[__table__.c.PackageID], + ) License = relationship( - _License, backref=backref("package_licenses", lazy="dynamic", - cascade="all, delete"), - foreign_keys=[__table__.c.LicenseID]) + _License, + backref=backref("package_licenses", lazy="dynamic", cascade="all, delete"), + foreign_keys=[__table__.c.LicenseID], + ) def __init__(self, **kwargs): super().__init__(**kwargs) @@ -31,10 +31,12 @@ class PackageLicense(Base): raise IntegrityError( statement="Primary key PackageID cannot be null.", orig="PackageLicenses.PackageID", - params=("NULL")) + params=("NULL"), + ) if not self.License and not self.LicenseID: raise IntegrityError( statement="Primary key LicenseID cannot be null.", orig="PackageLicenses.LicenseID", - params=("NULL")) + params=("NULL"), + ) diff --git a/aurweb/models/package_notification.py b/aurweb/models/package_notification.py index 327b92a6..7d5489d4 100644 --- a/aurweb/models/package_notification.py +++ b/aurweb/models/package_notification.py @@ -10,20 +10,19 @@ from aurweb.models.user import User as _User class PackageNotification(Base): __table__ = schema.PackageNotifications __tablename__ = __table__.name - __mapper_args__ = { - "primary_key": [__table__.c.UserID, __table__.c.PackageBaseID] - } + __mapper_args__ = {"primary_key": [__table__.c.UserID, __table__.c.PackageBaseID]} User = relationship( - _User, backref=backref("notifications", lazy="dynamic", - cascade="all, delete"), - foreign_keys=[__table__.c.UserID]) + _User, + backref=backref("notifications", lazy="dynamic", cascade="all, delete"), + foreign_keys=[__table__.c.UserID], + ) PackageBase = relationship( _PackageBase, - backref=backref("notifications", lazy="dynamic", - cascade="all, delete"), - foreign_keys=[__table__.c.PackageBaseID]) + backref=backref("notifications", lazy="dynamic", cascade="all, delete"), + foreign_keys=[__table__.c.PackageBaseID], + ) def __init__(self, **kwargs): super().__init__(**kwargs) @@ -32,10 +31,12 @@ class PackageNotification(Base): raise IntegrityError( statement="Foreign key UserID cannot be null.", orig="PackageNotifications.UserID", - params=("NULL")) + params=("NULL"), + ) if not self.PackageBase and not self.PackageBaseID: raise IntegrityError( statement="Foreign key PackageBaseID cannot be null.", orig="PackageNotifications.PackageBaseID", - params=("NULL")) + params=("NULL"), + ) diff --git a/aurweb/models/package_relation.py b/aurweb/models/package_relation.py index 4910934d..60988a56 100644 --- a/aurweb/models/package_relation.py +++ b/aurweb/models/package_relation.py @@ -19,13 +19,16 @@ class PackageRelation(Base): } Package = relationship( - _Package, backref=backref("package_relations", lazy="dynamic", - cascade="all, delete"), - foreign_keys=[__table__.c.PackageID]) + _Package, + backref=backref("package_relations", lazy="dynamic", cascade="all, delete"), + foreign_keys=[__table__.c.PackageID], + ) RelationType = relationship( - _RelationType, backref=backref("package_relations", lazy="dynamic"), - foreign_keys=[__table__.c.RelTypeID]) + _RelationType, + backref=backref("package_relations", lazy="dynamic"), + foreign_keys=[__table__.c.RelTypeID], + ) def __init__(self, **kwargs): super().__init__(**kwargs) @@ -34,16 +37,19 @@ class PackageRelation(Base): raise IntegrityError( statement="Foreign key PackageID cannot be null.", orig="PackageRelations.PackageID", - params=("NULL")) + params=("NULL"), + ) if not self.RelationType and not self.RelTypeID: raise IntegrityError( statement="Foreign key RelTypeID cannot be null.", orig="PackageRelations.RelTypeID", - params=("NULL")) + params=("NULL"), + ) if not self.RelName: raise IntegrityError( statement="Column RelName cannot be null.", orig="PackageRelations.RelName", - params=("NULL")) + params=("NULL"), + ) diff --git a/aurweb/models/package_request.py b/aurweb/models/package_request.py index 9669ec46..94ff064b 100644 --- a/aurweb/models/package_request.py +++ b/aurweb/models/package_request.py @@ -1,7 +1,10 @@ +import base64 +import hashlib + from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import backref, relationship -from aurweb import schema +from aurweb import config, schema from aurweb.models.declarative import Base from aurweb.models.package_base import PackageBase as _PackageBase from aurweb.models.request_type import RequestType as _RequestType @@ -25,26 +28,34 @@ class PackageRequest(Base): __mapper_args__ = {"primary_key": [__table__.c.ID]} RequestType = relationship( - _RequestType, backref=backref("package_requests", lazy="dynamic"), - foreign_keys=[__table__.c.ReqTypeID]) + _RequestType, + backref=backref("package_requests", lazy="dynamic"), + foreign_keys=[__table__.c.ReqTypeID], + ) User = relationship( - _User, backref=backref("package_requests", lazy="dynamic"), - foreign_keys=[__table__.c.UsersID]) + _User, + backref=backref("package_requests", lazy="dynamic"), + foreign_keys=[__table__.c.UsersID], + ) PackageBase = relationship( - _PackageBase, backref=backref("requests", lazy="dynamic"), - foreign_keys=[__table__.c.PackageBaseID]) + _PackageBase, + backref=backref("requests", lazy="dynamic"), + foreign_keys=[__table__.c.PackageBaseID], + ) Closer = relationship( - _User, backref=backref("closed_requests", lazy="dynamic"), - foreign_keys=[__table__.c.ClosedUID]) + _User, + backref=backref("closed_requests", lazy="dynamic"), + foreign_keys=[__table__.c.ClosedUID], + ) STATUS_DISPLAY = { PENDING_ID: PENDING, CLOSED_ID: CLOSED, ACCEPTED_ID: ACCEPTED, - REJECTED_ID: REJECTED + REJECTED_ID: REJECTED, } def __init__(self, **kwargs): @@ -54,38 +65,57 @@ class PackageRequest(Base): raise IntegrityError( statement="Foreign key ReqTypeID cannot be null.", orig="PackageRequests.ReqTypeID", - params=("NULL")) + params=("NULL"), + ) if not self.PackageBase and not self.PackageBaseID: raise IntegrityError( statement="Foreign key PackageBaseID cannot be null.", orig="PackageRequests.PackageBaseID", - params=("NULL")) + params=("NULL"), + ) if not self.PackageBaseName: raise IntegrityError( statement="Column PackageBaseName cannot be null.", orig="PackageRequests.PackageBaseName", - params=("NULL")) + params=("NULL"), + ) if not self.User and not self.UsersID: raise IntegrityError( statement="Foreign key UsersID cannot be null.", orig="PackageRequests.UsersID", - params=("NULL")) + params=("NULL"), + ) if self.Comments is None: raise IntegrityError( statement="Column Comments cannot be null.", orig="PackageRequests.Comments", - params=("NULL")) + params=("NULL"), + ) if self.ClosureComment is None: raise IntegrityError( statement="Column ClosureComment cannot be null.", orig="PackageRequests.ClosureComment", - params=("NULL")) + params=("NULL"), + ) def status_display(self) -> str: - """ Return a display string for the Status column. """ + """Return a display string for the Status column.""" return self.STATUS_DISPLAY[self.Status] + + def ml_message_id_hash(self) -> str: + """Return the X-Message-ID-Hash that is used in the mailing list archive.""" + # X-Message-ID-Hash is a base32 encoded SHA1 hash + msgid = f"pkg-request-{str(self.ID)}@aur.archlinux.org" + sha1 = hashlib.sha1(msgid.encode()).digest() + + return base64.b32encode(sha1).decode() + + def ml_message_url(self) -> str: + """Return the mailing list URL for the request.""" + url = config.get("options", "ml_thread_url") % (self.ml_message_id_hash()) + return url diff --git a/aurweb/models/package_source.py b/aurweb/models/package_source.py index 4ea1645b..a6d0f958 100644 --- a/aurweb/models/package_source.py +++ b/aurweb/models/package_source.py @@ -9,17 +9,13 @@ from aurweb.models.package import Package as _Package class PackageSource(Base): __table__ = schema.PackageSources __tablename__ = __table__.name - __mapper_args__ = { - "primary_key": [ - __table__.c.PackageID, - __table__.c.Source - ] - } + __mapper_args__ = {"primary_key": [__table__.c.PackageID, __table__.c.Source]} Package = relationship( - _Package, backref=backref("package_sources", lazy="dynamic", - cascade="all, delete"), - foreign_keys=[__table__.c.PackageID]) + _Package, + backref=backref("package_sources", lazy="dynamic", cascade="all, delete"), + foreign_keys=[__table__.c.PackageID], + ) def __init__(self, **kwargs): super().__init__(**kwargs) @@ -28,7 +24,8 @@ class PackageSource(Base): raise IntegrityError( statement="Foreign key PackageID cannot be null.", orig="PackageSources.PackageID", - params=("NULL")) + params=("NULL"), + ) if not self.Source: self.Source = "/dev/null" diff --git a/aurweb/models/package_vote.py b/aurweb/models/package_vote.py index 8da88210..b9e233d9 100644 --- a/aurweb/models/package_vote.py +++ b/aurweb/models/package_vote.py @@ -10,18 +10,19 @@ from aurweb.models.user import User as _User class PackageVote(Base): __table__ = schema.PackageVotes __tablename__ = __table__.name - __mapper_args__ = { - "primary_key": [__table__.c.UsersID, __table__.c.PackageBaseID] - } + __mapper_args__ = {"primary_key": [__table__.c.UsersID, __table__.c.PackageBaseID]} User = relationship( - _User, backref=backref("package_votes", lazy="dynamic"), - foreign_keys=[__table__.c.UsersID]) + _User, + backref=backref("package_votes", lazy="dynamic", cascade="all, delete"), + foreign_keys=[__table__.c.UsersID], + ) PackageBase = relationship( - _PackageBase, backref=backref("package_votes", lazy="dynamic", - cascade="all, delete"), - foreign_keys=[__table__.c.PackageBaseID]) + _PackageBase, + backref=backref("package_votes", lazy="dynamic", cascade="all, delete"), + foreign_keys=[__table__.c.PackageBaseID], + ) def __init__(self, **kwargs): super().__init__(**kwargs) @@ -30,16 +31,19 @@ class PackageVote(Base): raise IntegrityError( statement="Foreign key UsersID cannot be null.", orig="PackageVotes.UsersID", - params=("NULL")) + params=("NULL"), + ) if not self.PackageBase and not self.PackageBaseID: raise IntegrityError( statement="Foreign key PackageBaseID cannot be null.", orig="PackageVotes.PackageBaseID", - params=("NULL")) + params=("NULL"), + ) if not self.VoteTS: raise IntegrityError( statement="Column VoteTS cannot be null.", orig="PackageVotes.VoteTS", - params=("NULL")) + params=("NULL"), + ) diff --git a/aurweb/models/request_type.py b/aurweb/models/request_type.py index cabab3d2..1853b0be 100644 --- a/aurweb/models/request_type.py +++ b/aurweb/models/request_type.py @@ -16,5 +16,5 @@ class RequestType(Base): __mapper_args__ = {"primary_key": [__table__.c.ID]} def name_display(self) -> str: - """ Return the Name column with its first char capitalized. """ + """Return the Name column with its first char capitalized.""" return self.Name.title() diff --git a/aurweb/models/session.py b/aurweb/models/session.py index 37ab4bce..ff97f017 100644 --- a/aurweb/models/session.py +++ b/aurweb/models/session.py @@ -12,8 +12,10 @@ class Session(Base): __mapper_args__ = {"primary_key": [__table__.c.UsersID]} User = relationship( - _User, backref=backref("session", uselist=False), - foreign_keys=[__table__.c.UsersID]) + _User, + backref=backref("session", cascade="all, delete", uselist=False), + foreign_keys=[__table__.c.UsersID], + ) def __init__(self, **kwargs): super().__init__(**kwargs) @@ -29,10 +31,13 @@ class Session(Base): user_exists = db.query(_User).filter(_User.ID == uid).exists() if not db.query(user_exists).scalar(): raise IntegrityError( - statement=("Foreign key UsersID cannot be null and " - "must be a valid user's ID."), + statement=( + "Foreign key UsersID cannot be null and " + "must be a valid user's ID." + ), orig="Sessions.UsersID", - params=("NULL")) + params=("NULL"), + ) def generate_unique_sid(): diff --git a/aurweb/models/ssh_pub_key.py b/aurweb/models/ssh_pub_key.py index 53c8c3ac..c0b59445 100644 --- a/aurweb/models/ssh_pub_key.py +++ b/aurweb/models/ssh_pub_key.py @@ -12,16 +12,17 @@ class SSHPubKey(Base): __mapper_args__ = {"primary_key": [__table__.c.Fingerprint]} User = relationship( - "User", backref=backref("ssh_pub_keys", lazy="dynamic"), - foreign_keys=[__table__.c.UserID]) + "User", + backref=backref("ssh_pub_keys", lazy="dynamic", cascade="all, delete"), + foreign_keys=[__table__.c.UserID], + ) def __init__(self, **kwargs): super().__init__(**kwargs) def get_fingerprint(pubkey: str) -> str: - proc = Popen(["ssh-keygen", "-l", "-f", "-"], stdin=PIPE, stdout=PIPE, - stderr=PIPE) + proc = Popen(["ssh-keygen", "-l", "-f", "-"], stdin=PIPE, stdout=PIPE, stderr=PIPE) out, _ = proc.communicate(pubkey.encode()) if proc.returncode: raise ValueError("The SSH public key is invalid.") diff --git a/aurweb/models/term.py b/aurweb/models/term.py index 59534bbc..3aad9884 100644 --- a/aurweb/models/term.py +++ b/aurweb/models/term.py @@ -16,10 +16,12 @@ class Term(Base): raise IntegrityError( statement="Column Description cannot be null.", orig="Terms.Description", - params=("NULL")) + params=("NULL"), + ) if not self.URL: raise IntegrityError( statement="Column URL cannot be null.", orig="Terms.URL", - params=("NULL")) + params=("NULL"), + ) diff --git a/aurweb/models/user.py b/aurweb/models/user.py index c375fcbc..ee2889d2 100644 --- a/aurweb/models/user.py +++ b/aurweb/models/user.py @@ -1,9 +1,7 @@ import hashlib - -from typing import List, Set +from typing import Set import bcrypt - from fastapi import Request from sqlalchemy import or_ from sqlalchemy.exc import IntegrityError @@ -12,19 +10,19 @@ from sqlalchemy.orm import backref, relationship import aurweb.config import aurweb.models.account_type import aurweb.schema - -from aurweb import db, logging, schema, time, util +from aurweb import aur_logging, db, schema, time, util from aurweb.models.account_type import AccountType as _AccountType from aurweb.models.ban import is_banned from aurweb.models.declarative import Base -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) SALT_ROUNDS_DEFAULT = 12 class User(Base): - """ An ORM model of a single Users record. """ + """An ORM model of a single Users record.""" + __table__ = schema.Users __tablename__ = __table__.name __mapper_args__ = {"primary_key": [__table__.c.ID]} @@ -33,7 +31,8 @@ class User(Base): _AccountType, backref=backref("users", lazy="dynamic"), foreign_keys=[__table__.c.AccountTypeID], - uselist=False) + uselist=False, + ) # High-level variables used to track authentication (not in DB). authenticated = False @@ -41,50 +40,50 @@ class User(Base): # Make this static to the class just in case SQLAlchemy ever # does something to bypass our constructor. - salt_rounds = aurweb.config.getint("options", "salt_rounds", - SALT_ROUNDS_DEFAULT) + salt_rounds = aurweb.config.getint("options", "salt_rounds", SALT_ROUNDS_DEFAULT) def __init__(self, Passwd: str = str(), **kwargs): super().__init__(**kwargs, Passwd=str()) # Run this again in the constructor in case we rehashed config. - self.salt_rounds = aurweb.config.getint("options", "salt_rounds", - SALT_ROUNDS_DEFAULT) + self.salt_rounds = aurweb.config.getint( + "options", "salt_rounds", SALT_ROUNDS_DEFAULT + ) if Passwd: self.update_password(Passwd) def update_password(self, password): self.Passwd = bcrypt.hashpw( - password.encode(), - bcrypt.gensalt(rounds=self.salt_rounds)).decode() + password.encode(), bcrypt.gensalt(rounds=self.salt_rounds) + ).decode() @staticmethod def minimum_passwd_length(): return aurweb.config.getint("options", "passwd_min_len") def is_authenticated(self): - """ Return internal authenticated state. """ + """Return internal authenticated state.""" return self.authenticated def valid_password(self, password: str): - """ Check authentication against a given password. """ + """Check authentication against a given password.""" if password is None: return False password_is_valid = False try: - password_is_valid = bcrypt.checkpw(password.encode(), - self.Passwd.encode()) + password_is_valid = bcrypt.checkpw(password.encode(), self.Passwd.encode()) except ValueError: pass # If our Salt column is not empty, we're using a legacy password. if not password_is_valid and self.Salt != str(): # Try to login with legacy method. - password_is_valid = hashlib.md5( - f"{self.Salt}{password}".encode() - ).hexdigest() == self.Passwd + password_is_valid = ( + hashlib.md5(f"{self.Salt}{password}".encode()).hexdigest() + == self.Passwd + ) # We got here, we passed the legacy authentication. # Update the password to our modern hash style. @@ -96,9 +95,8 @@ class User(Base): def _login_approved(self, request: Request): return not is_banned(request) and not self.Suspended - def login(self, request: Request, password: str, - session_time: int = 0) -> str: - """ Login and authenticate a request. """ + def login(self, request: Request, password: str) -> str: + """Login and authenticate a request.""" from aurweb import db from aurweb.models.session import Session, generate_unique_sid @@ -124,12 +122,12 @@ class User(Base): try: with db.begin(): self.LastLogin = now_ts - self.LastLoginIPAddress = request.client.host + self.LastLoginIPAddress = util.get_client_ip(request) if not self.session: sid = generate_unique_sid() - self.session = db.create(Session, User=self, - SessionID=sid, - LastUpdateTS=now_ts) + self.session = db.create( + Session, User=self, SessionID=sid, LastUpdateTS=now_ts + ) else: last_updated = self.session.LastUpdateTS if last_updated and last_updated < now_ts: @@ -148,36 +146,36 @@ class User(Base): return self.session.SessionID - def has_credential(self, credential: Set[int], - approved: List["User"] = list()): + def has_credential(self, credential: Set[int], approved: list["User"] = list()): from aurweb.auth.creds import has_credential + return has_credential(self, credential, approved) - def logout(self, request: Request): + def logout(self, request: Request) -> None: self.authenticated = False if self.session: with db.begin(): db.delete(self.session) - def is_trusted_user(self): + def is_package_maintainer(self): return self.AccountType.ID in { - aurweb.models.account_type.TRUSTED_USER_ID, - aurweb.models.account_type.TRUSTED_USER_AND_DEV_ID + aurweb.models.account_type.PACKAGE_MAINTAINER_ID, + aurweb.models.account_type.PACKAGE_MAINTAINER_AND_DEV_ID, } def is_developer(self): return self.AccountType.ID in { aurweb.models.account_type.DEVELOPER_ID, - aurweb.models.account_type.TRUSTED_USER_AND_DEV_ID + aurweb.models.account_type.PACKAGE_MAINTAINER_AND_DEV_ID, } def is_elevated(self): - """ A User is 'elevated' when they have either a - Trusted User or Developer AccountType. """ + """A User is 'elevated' when they have either a + Package Maintainer or Developer AccountType.""" return self.AccountType.ID in { - aurweb.models.account_type.TRUSTED_USER_ID, + aurweb.models.account_type.PACKAGE_MAINTAINER_ID, aurweb.models.account_type.DEVELOPER_ID, - aurweb.models.account_type.TRUSTED_USER_AND_DEV_ID, + aurweb.models.account_type.PACKAGE_MAINTAINER_AND_DEV_ID, } def can_edit_user(self, target: "User") -> bool: @@ -190,24 +188,28 @@ class User(Base): In short, a user must at least have credentials and be at least the same account type as the target. - User < Trusted User < Developer < Trusted User & Developer + User < Package Maintainer < Developer < Package Maintainer & Developer :param target: Target User to be edited :return: Boolean indicating whether `self` can edit `target` """ from aurweb.auth import creds + has_cred = self.has_credential(creds.ACCOUNT_EDIT, approved=[target]) return has_cred and self.AccountTypeID >= target.AccountTypeID def voted_for(self, package) -> bool: - """ Has this User voted for package? """ + """Has this User voted for package?""" from aurweb.models.package_vote import PackageVote - return bool(package.PackageBase.package_votes.filter( - PackageVote.UsersID == self.ID - ).scalar()) + + return bool( + package.PackageBase.package_votes.filter( + PackageVote.UsersID == self.ID + ).scalar() + ) def notified(self, package) -> bool: - """ Is this User being notified about package (or package base)? + """Is this User being notified about package (or package base)? :param package: Package or PackageBase instance :return: Boolean indicating state of package notification @@ -225,12 +227,14 @@ class User(Base): # Run an exists() query where a pkgbase-related # PackageNotification exists for self (a user). - return bool(db.query( - query.filter(PackageNotification.UserID == self.ID).exists() - ).scalar()) + return bool( + db.query( + query.filter(PackageNotification.UserID == self.ID).exists() + ).scalar() + ) def packages(self): - """ Returns an ORM query to Package objects owned by this user. + """Returns an ORM query to Package objects owned by this user. This should really be replaced with an internal ORM join configured for the User model. This has not been done yet @@ -241,16 +245,24 @@ class User(Base): """ from aurweb.models.package import Package from aurweb.models.package_base import PackageBase - return db.query(Package).join(PackageBase).filter( - or_( - PackageBase.PackagerUID == self.ID, - PackageBase.MaintainerUID == self.ID + + return ( + db.query(Package) + .join(PackageBase) + .filter( + or_( + PackageBase.PackagerUID == self.ID, + PackageBase.MaintainerUID == self.ID, + ) ) ) def __repr__(self): return "" % ( - self.ID, str(self.AccountType), self.Username) + self.ID, + str(self.AccountType), + self.Username, + ) def __str__(self) -> str: return self.Username diff --git a/aurweb/models/tu_vote.py b/aurweb/models/vote.py similarity index 53% rename from aurweb/models/tu_vote.py rename to aurweb/models/vote.py index efb23b19..ec20fe9b 100644 --- a/aurweb/models/tu_vote.py +++ b/aurweb/models/vote.py @@ -3,24 +3,26 @@ from sqlalchemy.orm import backref, relationship from aurweb import schema from aurweb.models.declarative import Base -from aurweb.models.tu_voteinfo import TUVoteInfo as _TUVoteInfo from aurweb.models.user import User as _User +from aurweb.models.voteinfo import VoteInfo as _VoteInfo -class TUVote(Base): - __table__ = schema.TU_Votes +class Vote(Base): + __table__ = schema.Votes __tablename__ = __table__.name - __mapper_args__ = { - "primary_key": [__table__.c.VoteID, __table__.c.UserID] - } + __mapper_args__ = {"primary_key": [__table__.c.VoteID, __table__.c.UserID]} VoteInfo = relationship( - _TUVoteInfo, backref=backref("tu_votes", lazy="dynamic"), - foreign_keys=[__table__.c.VoteID]) + _VoteInfo, + backref=backref("votes", lazy="dynamic"), + foreign_keys=[__table__.c.VoteID], + ) User = relationship( - _User, backref=backref("tu_votes", lazy="dynamic"), - foreign_keys=[__table__.c.UserID]) + _User, + backref=backref("votes", lazy="dynamic"), + foreign_keys=[__table__.c.UserID], + ) def __init__(self, **kwargs): super().__init__(**kwargs) @@ -28,11 +30,13 @@ class TUVote(Base): if not self.VoteInfo and not self.VoteID: raise IntegrityError( statement="Foreign key VoteID cannot be null.", - orig="TU_Votes.VoteID", - params=("NULL")) + orig="Votes.VoteID", + params=("NULL"), + ) if not self.User and not self.UserID: raise IntegrityError( statement="Foreign key UserID cannot be null.", - orig="TU_Votes.UserID", - params=("NULL")) + orig="Votes.UserID", + params=("NULL"), + ) diff --git a/aurweb/models/tu_voteinfo.py b/aurweb/models/voteinfo.py similarity index 69% rename from aurweb/models/tu_voteinfo.py rename to aurweb/models/voteinfo.py index 7934a772..b7480661 100644 --- a/aurweb/models/tu_voteinfo.py +++ b/aurweb/models/voteinfo.py @@ -8,14 +8,16 @@ from aurweb.models.declarative import Base from aurweb.models.user import User as _User -class TUVoteInfo(Base): - __table__ = schema.TU_VoteInfo +class VoteInfo(Base): + __table__ = schema.VoteInfo __tablename__ = __table__.name __mapper_args__ = {"primary_key": [__table__.c.ID]} Submitter = relationship( - _User, backref=backref("tu_voteinfo_set", lazy="dynamic"), - foreign_keys=[__table__.c.SubmitterID]) + _User, + backref=backref("voteinfo_set", lazy="dynamic"), + foreign_keys=[__table__.c.SubmitterID], + ) def __init__(self, **kwargs): # Default Quorum, Yes, No and Abstain columns to 0. @@ -28,41 +30,46 @@ class TUVoteInfo(Base): if self.Agenda is None: raise IntegrityError( statement="Column Agenda cannot be null.", - orig="TU_VoteInfo.Agenda", - params=("NULL")) + orig="VoteInfo.Agenda", + params=("NULL"), + ) if self.User is None: raise IntegrityError( statement="Column User cannot be null.", - orig="TU_VoteInfo.User", - params=("NULL")) + orig="VoteInfo.User", + params=("NULL"), + ) if self.Submitted is None: raise IntegrityError( statement="Column Submitted cannot be null.", - orig="TU_VoteInfo.Submitted", - params=("NULL")) + orig="VoteInfo.Submitted", + params=("NULL"), + ) if self.End is None: raise IntegrityError( statement="Column End cannot be null.", - orig="TU_VoteInfo.End", - params=("NULL")) + orig="VoteInfo.End", + params=("NULL"), + ) if not self.Submitter: raise IntegrityError( statement="Foreign key SubmitterID cannot be null.", - orig="TU_VoteInfo.SubmitterID", - params=("NULL")) + orig="VoteInfo.SubmitterID", + params=("NULL"), + ) def __setattr__(self, key: str, value: typing.Any): - """ Customize setattr to stringify any Quorum keys given. """ + """Customize setattr to stringify any Quorum keys given.""" if key == "Quorum": value = str(value) return super().__setattr__(key, value) def __getattribute__(self, key: str): - """ Customize getattr to floatify any fetched Quorum values. """ + """Customize getattr to floatify any fetched Quorum values.""" attr = super().__getattribute__(key) if key == "Quorum": return float(attr) diff --git a/aurweb/packages/requests.py b/aurweb/packages/requests.py index 6aaa59ab..be35a3d3 100644 --- a/aurweb/packages/requests.py +++ b/aurweb/packages/requests.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Set +from typing import Optional, Set from fastapi import Request from sqlalchemy import and_, orm @@ -7,46 +7,55 @@ from aurweb import config, db, l10n, time, util from aurweb.exceptions import InvariantError from aurweb.models import PackageBase, PackageRequest, User from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID -from aurweb.models.request_type import DELETION, DELETION_ID, MERGE, MERGE_ID, ORPHAN, ORPHAN_ID +from aurweb.models.request_type import ( + DELETION, + DELETION_ID, + MERGE, + MERGE_ID, + ORPHAN, + ORPHAN_ID, +) from aurweb.scripts import notify class ClosureFactory: - """ A factory class used to autogenerate closure comments. """ + """A factory class used to autogenerate closure comments.""" - REQTYPE_NAMES = { - DELETION_ID: DELETION, - MERGE_ID: MERGE, - ORPHAN_ID: ORPHAN - } + REQTYPE_NAMES = {DELETION_ID: DELETION, MERGE_ID: MERGE, ORPHAN_ID: ORPHAN} - def _deletion_closure(self, requester: User, - pkgbase: PackageBase, - target: PackageBase = None): - return (f"[Autogenerated] Accepted deletion for {pkgbase.Name}.") + def _deletion_closure( + self, requester: User, pkgbase: PackageBase, target: PackageBase = None + ): + return f"[Autogenerated] Accepted deletion for {pkgbase.Name}." - def _merge_closure(self, requester: User, - pkgbase: PackageBase, - target: PackageBase = None): - return (f"[Autogenerated] Accepted merge for {pkgbase.Name} " - f"into {target.Name}.") + def _merge_closure( + self, requester: User, pkgbase: PackageBase, target: PackageBase = None + ): + return ( + f"[Autogenerated] Accepted merge for {pkgbase.Name} " f"into {target.Name}." + ) - def _orphan_closure(self, requester: User, - pkgbase: PackageBase, - target: PackageBase = None): - return (f"[Autogenerated] Accepted orphan for {pkgbase.Name}.") + def _orphan_closure( + self, requester: User, pkgbase: PackageBase, target: PackageBase = None + ): + return f"[Autogenerated] Accepted orphan for {pkgbase.Name}." - def _rejected_merge_closure(self, requester: User, - pkgbase: PackageBase, - target: PackageBase = None): - return (f"[Autogenerated] Another request to merge {pkgbase.Name} " - f"into {target.Name} has rendered this request invalid.") + def _rejected_merge_closure( + self, requester: User, pkgbase: PackageBase, target: PackageBase = None + ): + return ( + f"[Autogenerated] Another request to merge {pkgbase.Name} " + f"into {target.Name} has rendered this request invalid." + ) - def get_closure(self, reqtype_id: int, - requester: User, - pkgbase: PackageBase, - target: PackageBase = None, - status: int = ACCEPTED_ID) -> str: + def get_closure( + self, + reqtype_id: int, + requester: User, + pkgbase: PackageBase, + target: PackageBase = None, + status: int = ACCEPTED_ID, + ) -> str: """ Return a closure comment handled by this class. @@ -69,8 +78,9 @@ class ClosureFactory: return handler(requester, pkgbase, target) -def update_closure_comment(pkgbase: PackageBase, reqtype_id: int, - comments: str, target: PackageBase = None) -> None: +def update_closure_comment( + pkgbase: PackageBase, reqtype_id: int, comments: str, target: PackageBase = None +) -> None: """ Update all pending requests related to `pkgbase` with a closure comment. @@ -90,8 +100,10 @@ def update_closure_comment(pkgbase: PackageBase, reqtype_id: int, return query = pkgbase.requests.filter( - and_(PackageRequest.ReqTypeID == reqtype_id, - PackageRequest.Status == PENDING_ID)) + and_( + PackageRequest.ReqTypeID == reqtype_id, PackageRequest.Status == PENDING_ID + ) + ) if reqtype_id == MERGE_ID: query = query.filter(PackageRequest.MergeBaseName == target.Name) @@ -100,9 +112,8 @@ def update_closure_comment(pkgbase: PackageBase, reqtype_id: int, def verify_orphan_request(user: User, pkgbase: PackageBase): - """ Verify that an undue orphan request exists in `requests`. """ - requests = pkgbase.requests.filter( - PackageRequest.ReqTypeID == ORPHAN_ID) + """Verify that an undue orphan request exists in `requests`.""" + requests = pkgbase.requests.filter(PackageRequest.ReqTypeID == ORPHAN_ID) for pkgreq in requests: idle_time = config.getint("options", "request_idle_time") time_delta = time.utcnow() - pkgreq.RequestTS @@ -115,9 +126,13 @@ def verify_orphan_request(user: User, pkgbase: PackageBase): return False -def close_pkgreq(pkgreq: PackageRequest, closer: User, - pkgbase: PackageBase, target: Optional[PackageBase], - status: int) -> None: +def close_pkgreq( + pkgreq: PackageRequest, + closer: User, + pkgbase: PackageBase, + target: Optional[PackageBase], + status: int, +) -> None: """ Close a package request with `pkgreq`.Status == `status`. @@ -130,16 +145,20 @@ def close_pkgreq(pkgreq: PackageRequest, closer: User, now = time.utcnow() pkgreq.Status = status pkgreq.Closer = closer - pkgreq.ClosureComment = ( - pkgreq.ClosureComment or ClosureFactory().get_closure( - pkgreq.ReqTypeID, closer, pkgbase, target, status) + pkgreq.ClosureComment = pkgreq.ClosureComment or ClosureFactory().get_closure( + pkgreq.ReqTypeID, closer, pkgbase, target, status ) pkgreq.ClosedTS = now -def handle_request(request: Request, reqtype_id: int, - pkgbase: PackageBase, - target: PackageBase = None) -> List[notify.Notification]: +@db.retry_deadlock +def handle_request( + request: Request, + reqtype_id: int, + pkgbase: PackageBase, + target: PackageBase = None, + comments: str = str(), +) -> list[notify.Notification]: """ Handle package requests before performing an action. @@ -158,24 +177,27 @@ def handle_request(request: Request, reqtype_id: int, :param pkgbase: PackageBase which the request is about :param target: Optional target to merge into """ - notifs: List[notify.Notification] = [] + notifs: list[notify.Notification] = [] # If it's an orphan request, perform further verification # regarding existing requests. if reqtype_id == ORPHAN_ID: if not verify_orphan_request(request.user, pkgbase): _ = l10n.get_translator_for_request(request) - raise InvariantError(_( - "No due existing orphan requests to accept for %s." - ) % pkgbase.Name) + raise InvariantError( + _("No due existing orphan requests to accept for %s.") % pkgbase.Name + ) # Produce a base query for requests related to `pkgbase`, based # on ReqTypeID matching `reqtype_id`, pending status and a correct # PackagBaseName column. query: orm.Query = pkgbase.requests.filter( - and_(PackageRequest.ReqTypeID == reqtype_id, - PackageRequest.Status == PENDING_ID, - PackageRequest.PackageBaseName == pkgbase.Name)) + and_( + PackageRequest.ReqTypeID == reqtype_id, + PackageRequest.Status == PENDING_ID, + PackageRequest.PackageBaseName == pkgbase.Name, + ) + ) # Build a query for records we should accept. For merge requests, # this is specific to a matching MergeBaseName. For others, this @@ -183,17 +205,16 @@ def handle_request(request: Request, reqtype_id: int, accept_query: orm.Query = query if target: # If a `target` was supplied, filter by MergeBaseName - accept_query = query.filter( - PackageRequest.MergeBaseName == target.Name) + accept_query = query.filter(PackageRequest.MergeBaseName == target.Name) # Build an accept list out of `accept_query`. - to_accept: List[PackageRequest] = accept_query.all() + to_accept: list[PackageRequest] = accept_query.all() accepted_ids: Set[int] = set(p.ID for p in to_accept) # Build a reject list out of `query` filtered by IDs not found # in `to_accept`. That is, unmatched records of the same base # query properties. - to_reject: List[PackageRequest] = query.filter( + to_reject: list[PackageRequest] = query.filter( ~PackageRequest.ID.in_(accepted_ids) ).all() @@ -203,14 +224,16 @@ def handle_request(request: Request, reqtype_id: int, if not to_accept: utcnow = time.utcnow() with db.begin(): - pkgreq = db.create(PackageRequest, - ReqTypeID=reqtype_id, - RequestTS=utcnow, - User=request.user, - PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Comments="Autogenerated by aurweb.", - ClosureComment=str()) + pkgreq = db.create( + PackageRequest, + ReqTypeID=reqtype_id, + RequestTS=utcnow, + User=request.user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments="Autogenerated by aurweb.", + ClosureComment=comments, + ) # If it's a merge request, set MergeBaseName to `target`.Name. if pkgreq.ReqTypeID == MERGE_ID: @@ -221,16 +244,25 @@ def handle_request(request: Request, reqtype_id: int, to_accept.append(pkgreq) # Update requests with their new status and closures. - with db.begin(): - util.apply_all(to_accept, lambda p: close_pkgreq( - p, request.user, pkgbase, target, ACCEPTED_ID)) - util.apply_all(to_reject, lambda p: close_pkgreq( - p, request.user, pkgbase, target, REJECTED_ID)) + @db.retry_deadlock + def retry_closures(): + with db.begin(): + util.apply_all( + to_accept, + lambda p: close_pkgreq(p, request.user, pkgbase, target, ACCEPTED_ID), + ) + util.apply_all( + to_reject, + lambda p: close_pkgreq(p, request.user, pkgbase, target, REJECTED_ID), + ) + + retry_closures() # Create RequestCloseNotifications for all requests involved. - for pkgreq in (to_accept + to_reject): + for pkgreq in to_accept + to_reject: notif = notify.RequestCloseNotification( - request.user.ID, pkgreq.ID, pkgreq.status_display()) + request.user.ID, pkgreq.ID, pkgreq.status_display() + ) notifs.append(notif) # Return notifications to the caller for sending. diff --git a/aurweb/packages/search.py b/aurweb/packages/search.py index 4a6eb75f..78b27a9a 100644 --- a/aurweb/packages/search.py +++ b/aurweb/packages/search.py @@ -3,16 +3,23 @@ from typing import Set from sqlalchemy import and_, case, or_, orm from aurweb import db, models -from aurweb.models import Package, PackageBase, User -from aurweb.models.dependency_type import CHECKDEPENDS_ID, DEPENDS_ID, MAKEDEPENDS_ID, OPTDEPENDS_ID +from aurweb.models import Group, Package, PackageBase, User +from aurweb.models.dependency_type import ( + CHECKDEPENDS_ID, + DEPENDS_ID, + MAKEDEPENDS_ID, + OPTDEPENDS_ID, +) from aurweb.models.package_comaintainer import PackageComaintainer +from aurweb.models.package_group import PackageGroup from aurweb.models.package_keyword import PackageKeyword from aurweb.models.package_notification import PackageNotification from aurweb.models.package_vote import PackageVote +from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID class PackageSearch: - """ A Package search query builder. """ + """A Package search query builder.""" # A constant mapping of short to full name sort orderings. FULL_SORT_ORDER = {"d": "desc", "a": "asc"} @@ -24,14 +31,18 @@ class PackageSearch: if self.user: self.query = self.query.join( PackageVote, - and_(PackageVote.PackageBaseID == PackageBase.ID, - PackageVote.UsersID == self.user.ID), - isouter=True + and_( + PackageVote.PackageBaseID == PackageBase.ID, + PackageVote.UsersID == self.user.ID, + ), + isouter=True, ).join( PackageNotification, - and_(PackageNotification.PackageBaseID == PackageBase.ID, - PackageNotification.UserID == self.user.ID), - isouter=True + and_( + PackageNotification.PackageBaseID == PackageBase.ID, + PackageNotification.UserID == self.user.ID, + ), + isouter=True, ) self.ordering = "d" @@ -47,7 +58,7 @@ class PackageSearch: "m": self._search_by_maintainer, "c": self._search_by_comaintainer, "M": self._search_by_co_or_maintainer, - "s": self._search_by_submitter + "s": self._search_by_submitter, } # Setup SB (Sort By) callbacks. @@ -58,7 +69,7 @@ class PackageSearch: "w": self._sort_by_voted, "o": self._sort_by_notify, "m": self._sort_by_maintainer, - "l": self._sort_by_last_modified + "l": self._sort_by_last_modified, } self._joined_user = False @@ -66,12 +77,10 @@ class PackageSearch: self._joined_comaint = False def _join_user(self, outer: bool = True) -> orm.Query: - """ Centralized joining of a package base's maintainer. """ + """Centralized joining of a package base's maintainer.""" if not self._joined_user: self.query = self.query.join( - User, - User.ID == PackageBase.MaintainerUID, - isouter=outer + User, User.ID == PackageBase.MaintainerUID, isouter=outer ) self._joined_user = True return self.query @@ -87,7 +96,7 @@ class PackageSearch: self.query = self.query.join( PackageComaintainer, PackageComaintainer.PackageBaseID == PackageBase.ID, - isouter=isouter + isouter=isouter, ) self._joined_comaint = True return self.query @@ -95,8 +104,10 @@ class PackageSearch: def _search_by_namedesc(self, keywords: str) -> orm.Query: self._join_user() self.query = self.query.filter( - or_(Package.Name.like(f"%{keywords}%"), - Package.Description.like(f"%{keywords}%")) + or_( + Package.Name.like(f"%{keywords}%"), + Package.Description.like(f"%{keywords}%"), + ) ) return self @@ -125,15 +136,17 @@ class PackageSearch: self._join_user() self._join_keywords() keywords = set(k.lower() for k in keywords) - self.query = self.query.filter(PackageKeyword.Keyword.in_(keywords)) + self.query = self.query.filter(PackageKeyword.Keyword.in_(keywords)).group_by( + models.Package.Name + ) + return self def _search_by_maintainer(self, keywords: str) -> orm.Query: self._join_user() if keywords: self.query = self.query.filter( - and_(User.Username == keywords, - User.ID == PackageBase.MaintainerUID) + and_(User.Username == keywords, User.ID == PackageBase.MaintainerUID) ) else: self.query = self.query.filter(PackageBase.MaintainerUID.is_(None)) @@ -182,13 +195,13 @@ class PackageSearch: def _sort_by_votes(self, order: str): column = getattr(models.PackageBase.NumVotes, order) - name = getattr(models.Package.Name, order) + name = getattr(models.PackageBase.Name, order) self.query = self.query.order_by(column(), name()) return self def _sort_by_popularity(self, order: str): column = getattr(models.PackageBase.Popularity, order) - name = getattr(models.Package.Name, order) + name = getattr(models.PackageBase.Name, order) self.query = self.query.order_by(column(), name()) return self @@ -197,8 +210,7 @@ class PackageSearch: # in terms of performance. We should improve this; there's no # reason it should take _longer_. column = getattr( - case([(models.PackageVote.UsersID == self.user.ID, 1)], else_=0), - order + case([(models.PackageVote.UsersID == self.user.ID, 1)], else_=0), order ) name = getattr(models.Package.Name, order) self.query = self.query.order_by(column(), name()) @@ -209,9 +221,8 @@ class PackageSearch: # in terms of performance. We should improve this; there's no # reason it should take _longer_. column = getattr( - case([(models.PackageNotification.UserID == self.user.ID, 1)], - else_=0), - order + case([(models.PackageNotification.UserID == self.user.ID, 1)], else_=0), + order, ) name = getattr(models.Package.Name, order) self.query = self.query.order_by(column(), name()) @@ -225,7 +236,7 @@ class PackageSearch: def _sort_by_last_modified(self, order: str): column = getattr(models.PackageBase.ModifiedTS, order) - name = getattr(models.Package.Name, order) + name = getattr(models.PackageBase.Name, order) self.query = self.query.order_by(column(), name()) return self @@ -239,16 +250,16 @@ class PackageSearch: return callback(ordering) def count(self) -> int: - """ Return internal query's count. """ + """Return internal query's count.""" return self.query.count() def results(self) -> orm.Query: - """ Return internal query. """ + """Return internal query.""" return self.query class RPCSearch(PackageSearch): - """ A PackageSearch-derived RPC package search query builder. + """A PackageSearch-derived RPC package search query builder. With RPC search, we need a subset of PackageSearch's handlers, with a few additional handlers added. So, within the RPCSearch @@ -261,7 +272,7 @@ class RPCSearch(PackageSearch): sanitization done for the PackageSearch `by` argument. """ - keys_removed = ("b", "N", "B", "k", "c", "M", "s") + keys_removed = ("b", "N", "B", "M") def __init__(self) -> "RPCSearch": super().__init__() @@ -270,52 +281,112 @@ class RPCSearch(PackageSearch): # We keep: "nd", "n" and "m". We also overlay four new by params # on top: "depends", "makedepends", "optdepends" and "checkdepends". self.search_by_cb = { - k: v for k, v in self.search_by_cb.items() + k: v + for k, v in self.search_by_cb.items() if k not in RPCSearch.keys_removed } - self.search_by_cb.update({ - "depends": self._search_by_depends, - "makedepends": self._search_by_makedepends, - "optdepends": self._search_by_optdepends, - "checkdepends": self._search_by_checkdepends - }) + self.search_by_cb.update( + { + "depends": self._search_by_depends, + "makedepends": self._search_by_makedepends, + "optdepends": self._search_by_optdepends, + "checkdepends": self._search_by_checkdepends, + "provides": self._search_by_provides, + "conflicts": self._search_by_conflicts, + "replaces": self._search_by_replaces, + "groups": self._search_by_groups, + } + ) # We always want an optional Maintainer in the RPC. self._join_user() def _join_depends(self, dep_type_id: int) -> orm.Query: - """ Join Package with PackageDependency and filter results + """Join Package with PackageDependency and filter results based on `dep_type_id`. :param dep_type_id: DependencyType ID :returns: PackageDependency-joined orm.Query """ self.query = self.query.join(models.PackageDependency).filter( - models.PackageDependency.DepTypeID == dep_type_id) + models.PackageDependency.DepTypeID == dep_type_id + ) + return self.query + + def _join_relations(self, rel_type_id: int) -> orm.Query: + """Join Package with PackageRelation and filter results + based on `rel_type_id`. + + :param rel_type_id: RelationType ID + :returns: PackageRelation-joined orm.Query + """ + self.query = self.query.join(models.PackageRelation).filter( + models.PackageRelation.RelTypeID == rel_type_id + ) + return self.query + + def _join_groups(self) -> orm.Query: + """Join Package with PackageGroup and Group. + + :returns: PackageGroup/Group-joined orm.Query + """ + self.query = self.query.join(PackageGroup).join(Group) return self.query def _search_by_depends(self, keywords: str) -> "RPCSearch": self.query = self._join_depends(DEPENDS_ID).filter( - models.PackageDependency.DepName == keywords) + models.PackageDependency.DepName == keywords + ) return self def _search_by_makedepends(self, keywords: str) -> "RPCSearch": self.query = self._join_depends(MAKEDEPENDS_ID).filter( - models.PackageDependency.DepName == keywords) + models.PackageDependency.DepName == keywords + ) return self def _search_by_optdepends(self, keywords: str) -> "RPCSearch": self.query = self._join_depends(OPTDEPENDS_ID).filter( - models.PackageDependency.DepName == keywords) + models.PackageDependency.DepName == keywords + ) return self def _search_by_checkdepends(self, keywords: str) -> "RPCSearch": self.query = self._join_depends(CHECKDEPENDS_ID).filter( - models.PackageDependency.DepName == keywords) + models.PackageDependency.DepName == keywords + ) + return self + + def _search_by_provides(self, keywords: str) -> "RPCSearch": + self.query = self._join_relations(PROVIDES_ID).filter( + models.PackageRelation.RelName == keywords + ) + return self + + def _search_by_conflicts(self, keywords: str) -> "RPCSearch": + self.query = self._join_relations(CONFLICTS_ID).filter( + models.PackageRelation.RelName == keywords + ) + return self + + def _search_by_replaces(self, keywords: str) -> "RPCSearch": + self.query = self._join_relations(REPLACES_ID).filter( + models.PackageRelation.RelName == keywords + ) + return self + + def _search_by_groups(self, keywords: str) -> "RPCSearch": + self._join_groups() + self.query = self.query.filter(Group.Name == keywords) + return self + + def _search_by_keywords(self, keywords: str) -> "RPCSearch": + self._join_keywords() + self.query = self.query.filter(PackageKeyword.Keyword == keywords) return self def search_by(self, by: str, keywords: str) -> "RPCSearch": - """ Override inherited search_by. In this override, we reduce the + """Override inherited search_by. In this override, we reduce the scope of what we handle within this function. We do not set `by` to a default of "nd" in the RPC, as the RPC returns an error when incorrect `by` fields are specified. @@ -329,6 +400,4 @@ class RPCSearch(PackageSearch): return result def results(self) -> orm.Query: - return self.query.filter( - models.PackageBase.PackagerUID.isnot(None) - ) + return self.query diff --git a/aurweb/packages/util.py b/aurweb/packages/util.py index e8569f29..a2c6cbaa 100644 --- a/aurweb/packages/util.py +++ b/aurweb/packages/util.py @@ -1,21 +1,21 @@ from collections import defaultdict from http import HTTPStatus -from typing import Dict, List, Tuple, Union +from typing import Tuple, Union +from urllib.parse import quote_plus import orjson - from fastapi import HTTPException from sqlalchemy import orm from aurweb import config, db, models +from aurweb.aur_redis import redis_connection from aurweb.models import Package from aurweb.models.official_provider import OFFICIAL_BASE, OfficialProvider from aurweb.models.package_dependency import PackageDependency from aurweb.models.package_relation import PackageRelation -from aurweb.redis import redis_connection from aurweb.templates import register_filter -Providers = List[Union[PackageRelation, OfficialProvider]] +Providers = list[Union[PackageRelation, OfficialProvider]] def dep_extra_with_arch(dep: models.PackageDependency, annotation: str) -> str: @@ -43,10 +43,10 @@ def dep_optdepends_extra(dep: models.PackageDependency) -> str: @register_filter("dep_extra") def dep_extra(dep: models.PackageDependency) -> str: - """ Some dependency types have extra text added to their + """Some dependency types have extra text added to their display. This function provides that output. However, it **assumes** that the dep passed is bound to a valid one - of: depends, makedepends, checkdepends or optdepends. """ + of: depends, makedepends, checkdepends or optdepends.""" f = globals().get(f"dep_{dep.DependencyType.Name}_extra") return f(dep) @@ -61,13 +61,13 @@ def dep_extra_desc(dep: models.PackageDependency) -> str: @register_filter("pkgname_link") def pkgname_link(pkgname: str) -> str: - record = db.query(Package).filter( - Package.Name == pkgname).exists() + record = db.query(Package).filter(Package.Name == pkgname).exists() if db.query(record).scalar(): return f"/packages/{pkgname}" - official = db.query(OfficialProvider).filter( - OfficialProvider.Name == pkgname).exists() + official = ( + db.query(OfficialProvider).filter(OfficialProvider.Name == pkgname).exists() + ) if db.query(official).scalar(): base = "/".join([OFFICIAL_BASE, "packages"]) return f"{base}/?q={pkgname}" @@ -83,17 +83,17 @@ def package_link(package: Union[Package, OfficialProvider]) -> str: @register_filter("provides_markup") def provides_markup(provides: Providers) -> str: - return ", ".join([ - f'{pkg.Name}' - for pkg in provides - ]) + links = [] + for pkg in provides: + aur = "AUR" if not pkg.is_official else "" + links.append(f'{pkg.Name}{aur}') + return ", ".join(links) def get_pkg_or_base( - name: str, - cls: Union[models.Package, models.PackageBase] = models.PackageBase) \ - -> Union[models.Package, models.PackageBase]: - """ Get a PackageBase instance by its name or raise a 404 if + name: str, cls: Union[models.Package, models.PackageBase] = models.PackageBase +) -> Union[models.Package, models.PackageBase]: + """Get a PackageBase instance by its name or raise a 404 if it can't be found in the database. :param name: {Package,PackageBase}.Name @@ -102,15 +102,13 @@ def get_pkg_or_base( :raises HTTPException: With status code 404 if record doesn't exist :return: {Package,PackageBase} instance """ - with db.begin(): - instance = db.query(cls).filter(cls.Name == name).first() + instance = db.query(cls).filter(cls.Name == name).first() if not instance: raise HTTPException(status_code=HTTPStatus.NOT_FOUND) return instance -def get_pkgbase_comment(pkgbase: models.PackageBase, id: int) \ - -> models.PackageComment: +def get_pkgbase_comment(pkgbase: models.PackageBase, id: int) -> models.PackageComment: comment = pkgbase.comments.filter(models.PackageComment.ID == id).first() if not comment: raise HTTPException(status_code=HTTPStatus.NOT_FOUND) @@ -122,9 +120,8 @@ def out_of_date(packages: orm.Query) -> orm.Query: return packages.filter(models.PackageBase.OutOfDateTS.isnot(None)) -def updated_packages(limit: int = 0, - cache_ttl: int = 600) -> List[models.Package]: - """ Return a list of valid Package objects ordered by their +def updated_packages(limit: int = 0, cache_ttl: int = 600) -> list[models.Package]: + """Return a list of valid Package objects ordered by their ModifiedTS column in descending order from cache, after setting the cache when no key yet exists. @@ -138,27 +135,26 @@ def updated_packages(limit: int = 0, # If we already have a cache, deserialize it and return. return orjson.loads(packages) - with db.begin(): - query = db.query(models.Package).join(models.PackageBase).filter( - models.PackageBase.PackagerUID.isnot(None) - ).order_by( - models.PackageBase.ModifiedTS.desc() - ) + query = ( + db.query(models.Package) + .join(models.PackageBase) + .order_by(models.PackageBase.ModifiedTS.desc()) + ) - if limit: - query = query.limit(limit) + if limit: + query = query.limit(limit) packages = [] for pkg in query: # For each Package returned by the query, append a dict # containing Package columns we're interested in. - packages.append({ - "Name": pkg.Name, - "Version": pkg.Version, - "PackageBase": { - "ModifiedTS": pkg.PackageBase.ModifiedTS + packages.append( + { + "Name": pkg.Name, + "Version": pkg.Version, + "PackageBase": {"ModifiedTS": pkg.PackageBase.ModifiedTS}, } - }) + ) # Store the JSON serialization of the package_updates key into Redis. redis.set("package_updates", orjson.dumps(packages)) @@ -168,9 +164,8 @@ def updated_packages(limit: int = 0, return packages -def query_voted(query: List[models.Package], - user: models.User) -> Dict[int, bool]: - """ Produce a dictionary of package base ID keys to boolean values, +def query_voted(query: list[models.Package], user: models.User) -> dict[int, bool]: + """Produce a dictionary of package base ID keys to boolean values, which indicate whether or not the package base has a vote record related to user. @@ -180,20 +175,18 @@ def query_voted(query: List[models.Package], """ output = defaultdict(bool) query_set = {pkg.PackageBaseID for pkg in query} - voted = db.query(models.PackageVote).join( - models.PackageBase, - models.PackageBase.ID.in_(query_set) - ).filter( - models.PackageVote.UsersID == user.ID + voted = ( + db.query(models.PackageVote) + .join(models.PackageBase, models.PackageBase.ID.in_(query_set)) + .filter(models.PackageVote.UsersID == user.ID) ) for vote in voted: output[vote.PackageBase.ID] = True return output -def query_notified(query: List[models.Package], - user: models.User) -> Dict[int, bool]: - """ Produce a dictionary of package base ID keys to boolean values, +def query_notified(query: list[models.Package], user: models.User) -> dict[int, bool]: + """Produce a dictionary of package base ID keys to boolean values, which indicate whether or not the package base has a notification record related to user. @@ -203,19 +196,17 @@ def query_notified(query: List[models.Package], """ output = defaultdict(bool) query_set = {pkg.PackageBaseID for pkg in query} - notified = db.query(models.PackageNotification).join( - models.PackageBase, - models.PackageBase.ID.in_(query_set) - ).filter( - models.PackageNotification.UserID == user.ID + notified = ( + db.query(models.PackageNotification) + .join(models.PackageBase, models.PackageBase.ID.in_(query_set)) + .filter(models.PackageNotification.UserID == user.ID) ) for notif in notified: output[notif.PackageBase.ID] = True return output -def pkg_required(pkgname: str, provides: List[str], limit: int) \ - -> List[PackageDependency]: +def pkg_required(pkgname: str, provides: list[str]) -> list[PackageDependency]: """ Get dependencies that match a string in `[pkgname] + provides`. @@ -225,10 +216,14 @@ def pkg_required(pkgname: str, provides: List[str], limit: int) \ :return: List of PackageDependency instances """ targets = set([pkgname] + provides) - query = db.query(PackageDependency).join(Package).filter( - PackageDependency.DepName.in_(targets) - ).order_by(Package.Name.asc()).limit(limit) - return query.all() + query = ( + db.query(PackageDependency) + .join(Package) + .options(orm.contains_eager(PackageDependency.Package)) + .filter(PackageDependency.DepName.in_(targets)) + .order_by(Package.Name.asc()) + ) + return query @register_filter("source_uri") @@ -247,12 +242,12 @@ def source_uri(pkgsrc: models.PackageSource) -> Tuple[str, str]: the package base name. :param pkgsrc: PackageSource instance - :return (text, uri) tuple + :return text, uri)tuple """ if "::" in pkgsrc.Source: return pkgsrc.Source.split("::", 1) elif "://" in pkgsrc.Source: - return (pkgsrc.Source, pkgsrc.Source) + return pkgsrc.Source, pkgsrc.Source path = config.get("options", "source_file_uri") - pkgbasename = pkgsrc.Package.PackageBase.Name - return (pkgsrc.Source, path % (pkgsrc.Source, pkgbasename)) + pkgbasename = quote_plus(pkgsrc.Package.PackageBase.Name) + return pkgsrc.Source, path % (pkgsrc.Source, pkgbasename) diff --git a/aurweb/pkgbase/actions.py b/aurweb/pkgbase/actions.py index 229d52b9..d2471d8d 100644 --- a/aurweb/pkgbase/actions.py +++ b/aurweb/pkgbase/actions.py @@ -1,10 +1,8 @@ -from typing import List - from fastapi import Request -from aurweb import db, logging, util +from aurweb import aur_logging, db, util from aurweb.auth import creds -from aurweb.models import PackageBase +from aurweb.models import PackageBase, User from aurweb.models.package_comaintainer import PackageComaintainer from aurweb.models.package_notification import PackageNotification from aurweb.models.request_type import DELETION_ID, MERGE_ID, ORPHAN_ID @@ -12,19 +10,30 @@ from aurweb.packages.requests import handle_request, update_closure_comment from aurweb.pkgbase import util as pkgbaseutil from aurweb.scripts import notify, popupdate -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) + + +@db.retry_deadlock +def _retry_notify(user: User, pkgbase: PackageBase) -> None: + with db.begin(): + db.create(PackageNotification, PackageBase=pkgbase, User=user) def pkgbase_notify_instance(request: Request, pkgbase: PackageBase) -> None: - notif = db.query(pkgbase.notifications.filter( - PackageNotification.UserID == request.user.ID - ).exists()).scalar() + notif = db.query( + pkgbase.notifications.filter( + PackageNotification.UserID == request.user.ID + ).exists() + ).scalar() has_cred = request.user.has_credential(creds.PKGBASE_NOTIFY) if has_cred and not notif: - with db.begin(): - db.create(PackageNotification, - PackageBase=pkgbase, - User=request.user) + _retry_notify(request.user, pkgbase) + + +@db.retry_deadlock +def _retry_unnotify(notif: PackageNotification, pkgbase: PackageBase) -> None: + with db.begin(): + db.delete(notif) def pkgbase_unnotify_instance(request: Request, pkgbase: PackageBase) -> None: @@ -33,25 +42,38 @@ def pkgbase_unnotify_instance(request: Request, pkgbase: PackageBase) -> None: ).first() has_cred = request.user.has_credential(creds.PKGBASE_NOTIFY) if has_cred and notif: - with db.begin(): - db.delete(notif) + _retry_unnotify(notif, pkgbase) + + +@db.retry_deadlock +def _retry_unflag(pkgbase: PackageBase) -> None: + with db.begin(): + pkgbase.OutOfDateTS = None + pkgbase.Flagger = None + pkgbase.FlaggerComment = str() def pkgbase_unflag_instance(request: Request, pkgbase: PackageBase) -> None: has_cred = request.user.has_credential( - creds.PKGBASE_UNFLAG, approved=[pkgbase.Flagger, pkgbase.Maintainer]) + creds.PKGBASE_UNFLAG, + approved=[pkgbase.Flagger, pkgbase.Maintainer] + + [c.User for c in pkgbase.comaintainers], + ) if has_cred: - with db.begin(): - pkgbase.OutOfDateTS = None - pkgbase.Flagger = None - pkgbase.FlaggerComment = str() + _retry_unflag(pkgbase) -def pkgbase_disown_instance(request: Request, pkgbase: PackageBase) -> None: - disowner = request.user - notifs = [notify.DisownNotification(disowner.ID, pkgbase.ID)] +@db.retry_deadlock +def _retry_disown(request: Request, pkgbase: PackageBase): + notifs: list[notify.Notification] = [] + + is_maint = request.user == pkgbase.Maintainer + + comaint = pkgbase.comaintainers.filter( + PackageComaintainer.User == request.user + ).one_or_none() + is_comaint = comaint is not None - is_maint = disowner == pkgbase.Maintainer if is_maint: with db.begin(): # Comaintainer with the lowest Priority value; next-in-line. @@ -65,46 +87,61 @@ def pkgbase_disown_instance(request: Request, pkgbase: PackageBase) -> None: else: # Otherwise, just orphan the package completely. pkgbase.Maintainer = None + elif is_comaint: + # This disown request is from a Comaintainer + with db.begin(): + notif = pkgbaseutil.remove_comaintainer(comaint) + notifs.append(notif) elif request.user.has_credential(creds.PKGBASE_DISOWN): # Otherwise, the request user performing this disownage is a - # Trusted User and we treat it like a standard orphan request. + # Package Maintainer and we treat it like a standard orphan request. notifs += handle_request(request, ORPHAN_ID, pkgbase) with db.begin(): pkgbase.Maintainer = None db.delete_all(pkgbase.comaintainers) + return notifs + + +def pkgbase_disown_instance(request: Request, pkgbase: PackageBase) -> None: + disowner = request.user + notifs = [notify.DisownNotification(disowner.ID, pkgbase.ID)] + notifs += _retry_disown(request, pkgbase) util.apply_all(notifs, lambda n: n.send()) -def pkgbase_adopt_instance(request: Request, pkgbase: PackageBase) -> None: +@db.retry_deadlock +def _retry_adopt(request: Request, pkgbase: PackageBase) -> None: with db.begin(): pkgbase.Maintainer = request.user + +def pkgbase_adopt_instance(request: Request, pkgbase: PackageBase) -> None: + _retry_adopt(request, pkgbase) notif = notify.AdoptNotification(request.user.ID, pkgbase.ID) notif.send() -def pkgbase_delete_instance(request: Request, pkgbase: PackageBase, - comments: str = str()) \ - -> List[notify.Notification]: - notifs = handle_request(request, DELETION_ID, pkgbase) + [ - notify.DeleteNotification(request.user.ID, pkgbase.ID) - ] - +@db.retry_deadlock +def _retry_delete(pkgbase: PackageBase, comments: str) -> None: with db.begin(): update_closure_comment(pkgbase, DELETION_ID, comments) db.delete(pkgbase) + +def pkgbase_delete_instance( + request: Request, pkgbase: PackageBase, comments: str = str() +) -> list[notify.Notification]: + notif = notify.DeleteNotification(request.user.ID, pkgbase.ID) + notifs = handle_request(request, DELETION_ID, pkgbase, comments=comments) + [notif] + + _retry_delete(pkgbase, comments) + return notifs -def pkgbase_merge_instance(request: Request, pkgbase: PackageBase, - target: PackageBase, comments: str = str()) -> None: - pkgbasename = str(pkgbase.Name) - - # Create notifications. - notifs = handle_request(request, MERGE_ID, pkgbase, target) - +@db.retry_deadlock +def _retry_merge(pkgbase: PackageBase, target: PackageBase) -> None: # Target votes and notifications sets of user IDs that are # looking to be migrated. target_votes = set(v.UsersID for v in target.package_votes) @@ -134,9 +171,25 @@ def pkgbase_merge_instance(request: Request, pkgbase: PackageBase, db.delete(pkg) db.delete(pkgbase) + +def pkgbase_merge_instance( + request: Request, + pkgbase: PackageBase, + target: PackageBase, + comments: str = str(), +) -> None: + pkgbasename = str(pkgbase.Name) + + # Create notifications. + notifs = handle_request(request, MERGE_ID, pkgbase, target, comments) + + _retry_merge(pkgbase, target) + # Log this out for accountability purposes. - logger.info(f"Trusted User '{request.user.Username}' merged " - f"'{pkgbasename}' into '{target.Name}'.") + logger.info( + f"Package Maintainer '{request.user.Username}' merged " + f"'{pkgbasename}' into '{target.Name}'." + ) # Send notifications. util.apply_all(notifs, lambda n: n.send()) diff --git a/aurweb/pkgbase/util.py b/aurweb/pkgbase/util.py index 18af3df0..d3c2f35c 100644 --- a/aurweb/pkgbase/util.py +++ b/aurweb/pkgbase/util.py @@ -1,10 +1,12 @@ -from typing import Any, Dict, List +from typing import Any from fastapi import Request from sqlalchemy import and_ +from sqlalchemy.orm import joinedload -from aurweb import config, db, l10n, util +from aurweb import config, db, defaults, l10n, time, util from aurweb.models import PackageBase, User +from aurweb.models.package_base import popularity from aurweb.models.package_comaintainer import PackageComaintainer from aurweb.models.package_comment import PackageComment from aurweb.models.package_request import PENDING_ID, PackageRequest @@ -13,51 +15,88 @@ from aurweb.scripts import notify from aurweb.templates import make_context as _make_context -def make_context(request: Request, pkgbase: PackageBase) -> Dict[str, Any]: - """ Make a basic context for package or pkgbase. +def make_context( + request: Request, pkgbase: PackageBase, context: dict[str, Any] = None +) -> dict[str, Any]: + """Make a basic context for package or pkgbase. :param request: FastAPI request :param pkgbase: PackageBase instance :return: A pkgbase context without specific differences """ - context = _make_context(request, pkgbase.Name) + if not context: + context = _make_context(request, pkgbase.Name) + is_authenticated = request.user.is_authenticated() + + # Per page and offset. + offset, per_page = util.sanitize_params( + request.query_params.get("O", defaults.O), + request.query_params.get("PP", defaults.COMMENTS_PER_PAGE), + ) + context["O"] = offset + context["PP"] = per_page context["git_clone_uri_anon"] = config.get("options", "git_clone_uri_anon") context["git_clone_uri_priv"] = config.get("options", "git_clone_uri_priv") context["pkgbase"] = pkgbase context["comaintainers"] = [ - c.User for c in pkgbase.comaintainers.order_by( - PackageComaintainer.Priority.asc() - ).all() + c.User + for c in pkgbase.comaintainers.options(joinedload(PackageComaintainer.User)) + .order_by(PackageComaintainer.Priority.asc()) + .all() ] + if is_authenticated: + context["unflaggers"] = context["comaintainers"].copy() + context["unflaggers"].extend([pkgbase.Maintainer, pkgbase.Flagger]) + else: + context["unflaggers"] = [] + context["packages_count"] = pkgbase.packages.count() context["keywords"] = pkgbase.keywords - context["comments"] = pkgbase.comments.order_by( + context["comments_total"] = pkgbase.comments.order_by( PackageComment.CommentTS.desc() + ).count() + context["comments"] = ( + pkgbase.comments.order_by(PackageComment.CommentTS.desc()) + .limit(per_page) + .offset(offset) ) context["pinned_comments"] = pkgbase.comments.filter( PackageComment.PinnedTS != 0 ).order_by(PackageComment.CommentTS.desc()) context["is_maintainer"] = bool(request.user == pkgbase.Maintainer) - context["notified"] = request.user.notified(pkgbase) + if is_authenticated: + context["notified"] = request.user.notified(pkgbase) + else: + context["notified"] = False context["out_of_date"] = bool(pkgbase.OutOfDateTS) - context["voted"] = request.user.package_votes.filter( - PackageVote.PackageBaseID == pkgbase.ID - ).scalar() + if is_authenticated: + context["voted"] = db.query( + request.user.package_votes.filter( + PackageVote.PackageBaseID == pkgbase.ID + ).exists() + ).scalar() + else: + context["voted"] = False - context["requests"] = pkgbase.requests.filter( - and_(PackageRequest.Status == PENDING_ID, - PackageRequest.ClosedTS.is_(None)) - ).count() + if is_authenticated: + context["requests"] = pkgbase.requests.filter( + and_(PackageRequest.Status == PENDING_ID, PackageRequest.ClosedTS.is_(None)) + ).count() + else: + context["requests"] = [] + + context["popularity"] = popularity(pkgbase, time.utcnow()) return context -def remove_comaintainer(comaint: PackageComaintainer) \ - -> notify.ComaintainerRemoveNotification: +def remove_comaintainer( + comaint: PackageComaintainer, +) -> notify.ComaintainerRemoveNotification: """ Remove a PackageComaintainer. @@ -77,7 +116,8 @@ def remove_comaintainer(comaint: PackageComaintainer) \ return notif -def remove_comaintainers(pkgbase: PackageBase, usernames: List[str]) -> None: +@db.retry_deadlock +def remove_comaintainers(pkgbase: PackageBase, usernames: list[str]) -> None: """ Remove comaintainers from `pkgbase`. @@ -86,9 +126,9 @@ def remove_comaintainers(pkgbase: PackageBase, usernames: List[str]) -> None: """ notifications = [] with db.begin(): - comaintainers = pkgbase.comaintainers.join(User).filter( - User.Username.in_(usernames) - ).all() + comaintainers = ( + pkgbase.comaintainers.join(User).filter(User.Username.in_(usernames)).all() + ) notifications = [ notify.ComaintainerRemoveNotification(co.User.ID, pkgbase.ID) for co in comaintainers @@ -112,23 +152,24 @@ def latest_priority(pkgbase: PackageBase) -> int: """ # Order comaintainers related to pkgbase by Priority DESC. - record = pkgbase.comaintainers.order_by( - PackageComaintainer.Priority.desc()).first() + record = pkgbase.comaintainers.order_by(PackageComaintainer.Priority.desc()).first() # Use Priority column if record exists, otherwise 0. return record.Priority if record else 0 class NoopComaintainerNotification: - """ A noop notification stub used as an error-state return value. """ + """A noop notification stub used as an error-state return value.""" def send(self) -> None: - """ noop """ + """noop""" return -def add_comaintainer(pkgbase: PackageBase, comaintainer: User) \ - -> notify.ComaintainerAddNotification: +@db.retry_deadlock +def add_comaintainer( + pkgbase: PackageBase, comaintainer: User +) -> notify.ComaintainerAddNotification: """ Add a new comaintainer to `pkgbase`. @@ -144,14 +185,19 @@ def add_comaintainer(pkgbase: PackageBase, comaintainer: User) \ new_prio = latest_priority(pkgbase) + 1 with db.begin(): - db.create(PackageComaintainer, PackageBase=pkgbase, - User=comaintainer, Priority=new_prio) + db.create( + PackageComaintainer, + PackageBase=pkgbase, + User=comaintainer, + Priority=new_prio, + ) return notify.ComaintainerAddNotification(comaintainer.ID, pkgbase.ID) -def add_comaintainers(request: Request, pkgbase: PackageBase, - usernames: List[str]) -> None: +def add_comaintainers( + request: Request, pkgbase: PackageBase, usernames: list[str] +) -> None: """ Add comaintainers to `pkgbase`. @@ -195,7 +241,6 @@ def rotate_comaintainers(pkgbase: PackageBase) -> None: :param pkgbase: PackageBase instance """ - comaintainers = pkgbase.comaintainers.order_by( - PackageComaintainer.Priority.asc()) + comaintainers = pkgbase.comaintainers.order_by(PackageComaintainer.Priority.asc()) for i, comaint in enumerate(comaintainers): comaint.Priority = i + 1 diff --git a/aurweb/pkgbase/validate.py b/aurweb/pkgbase/validate.py index 8d05a3d7..b76e1a38 100644 --- a/aurweb/pkgbase/validate.py +++ b/aurweb/pkgbase/validate.py @@ -1,35 +1,55 @@ -from typing import Any, Dict +from http import HTTPStatus +from typing import Any -from aurweb import db +from fastapi import HTTPException + +from aurweb import config, db from aurweb.exceptions import ValidationError from aurweb.models import PackageBase -def request(pkgbase: PackageBase, - type: str, comments: str, merge_into: str, - context: Dict[str, Any]) -> None: - if not comments: - raise ValidationError(["The comment field must not be empty."]) +def request( + pkgbase: PackageBase, + type: str, + comments: str, + merge_into: str, + context: dict[str, Any], +) -> None: + # validate comment + comment(comments) if type == "merge": # Perform merge-related checks. if not merge_into: # TODO: This error needs to be translated. - raise ValidationError( - ['The "Merge into" field must not be empty.']) + raise ValidationError(['The "Merge into" field must not be empty.']) - target = db.query(PackageBase).filter( - PackageBase.Name == merge_into - ).first() + target = db.query(PackageBase).filter(PackageBase.Name == merge_into).first() if not target: # TODO: This error needs to be translated. - raise ValidationError([ - "The package base you want to merge into does not exist." - ]) + raise ValidationError( + ["The package base you want to merge into does not exist."] + ) db.refresh(target) if target.ID == pkgbase.ID: # TODO: This error needs to be translated. - raise ValidationError([ - "You cannot merge a package base into itself." - ]) + raise ValidationError(["You cannot merge a package base into itself."]) + + +def comment(comment: str): + if not comment: + raise ValidationError(["The comment field must not be empty."]) + + if len(comment) > config.getint("options", "max_chars_comment", 5000): + raise ValidationError(["Maximum number of characters for comment exceeded."]) + + +def comment_raise_http_ex(comments: str): + try: + comment(comments) + except ValidationError as err: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=err.data[0], + ) diff --git a/aurweb/prometheus.py b/aurweb/prometheus.py index 73be3ef6..40b99a90 100644 --- a/aurweb/prometheus.py +++ b/aurweb/prometheus.py @@ -1,26 +1,49 @@ -from typing import Any, Callable, Dict, List, Optional +from typing import Any, Callable, Optional -from prometheus_client import Counter +from prometheus_client import Counter, Gauge from prometheus_fastapi_instrumentator import Instrumentator from prometheus_fastapi_instrumentator.metrics import Info from starlette.routing import Match, Route -from aurweb import logging +from aurweb import aur_logging -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) _instrumentator = Instrumentator() +# Custom metrics +SEARCH_REQUESTS = Counter( + "aur_search_requests", "Number of search requests by cache hit/miss", ["cache"] +) +USERS = Gauge( + "aur_users", "Number of AUR users by type", ["type"], multiprocess_mode="livemax" +) +PACKAGES = Gauge( + "aur_packages", + "Number of AUR packages by state", + ["state"], + multiprocess_mode="livemax", +) +REQUESTS = Gauge( + "aur_requests", + "Number of AUR requests by type and status", + ["type", "status"], + multiprocess_mode="livemax", +) + + def instrumentator(): return _instrumentator +# FastAPI metrics # Taken from https://github.com/stephenhillier/starlette_exporter # Their license is included in LICENSES/starlette_exporter. # The code has been modified to remove child route checks # (since we don't have any) and to stay within an 80-width limit. -def get_matching_route_path(scope: Dict[Any, Any], routes: List[Route], - route_name: Optional[str] = None) -> str: +def get_matching_route_path( + scope: dict[Any, Any], routes: list[Route], route_name: Optional[str] = None +) -> str: """ Find a matching route and return its original path string @@ -34,7 +57,7 @@ def get_matching_route_path(scope: Dict[Any, Any], routes: List[Route], if match == Match.FULL: route_name = route.path - ''' + """ # This path exists in the original function's code, but we # don't need it (currently), so it's been removed to avoid # useless test coverage. @@ -47,7 +70,7 @@ def get_matching_route_path(scope: Dict[Any, Any], routes: List[Route], route_name = None else: route_name += child_route_name - ''' + """ return route_name elif match == Match.PARTIAL and route_name is None: @@ -55,11 +78,16 @@ def get_matching_route_path(scope: Dict[Any, Any], routes: List[Route], def http_requests_total() -> Callable[[Info], None]: - metric = Counter("http_requests_total", - "Number of HTTP requests.", - labelnames=("method", "path", "status")) + metric = Counter( + "http_requests_total", + "Number of HTTP requests.", + labelnames=("method", "path", "status"), + ) def instrumentation(info: Info) -> None: + if info.request.method.lower() in ("head", "options"): # pragma: no cover + return + scope = info.request.scope # Taken from https://github.com/stephenhillier/starlette_exporter @@ -70,19 +98,19 @@ def http_requests_total() -> Callable[[Info], None]: if not (scope.get("endpoint", None) and scope.get("router", None)): return None - root_path = scope.get("root_path", "") - app = scope.get("app", {}) + root_path = scope.get("root_path", str()) + app = scope.get("app", dict()) if hasattr(app, "root_path"): app_root_path = getattr(app, "root_path") if root_path.startswith(app_root_path): - root_path = root_path[len(app_root_path):] + root_path = root_path[len(app_root_path) :] base_scope = { "type": scope.get("type"), "path": root_path + scope.get("path"), "path_params": scope.get("path_params", {}), - "method": scope.get("method") + "method": scope.get("method"), } method = scope.get("method") @@ -99,9 +127,13 @@ def http_api_requests_total() -> Callable[[Info], None]: metric = Counter( "http_api_requests", "Number of times an RPC API type has been requested.", - labelnames=("type", "status")) + labelnames=("type", "status"), + ) def instrumentation(info: Info) -> None: + if info.request.method.lower() in ("head", "options"): # pragma: no cover + return + if info.request.url.path.rstrip("/") == "/rpc": type = info.request.query_params.get("type", "None") if info.response: diff --git a/aurweb/ratelimit.py b/aurweb/ratelimit.py index 659ab6b8..060f8dcb 100644 --- a/aurweb/ratelimit.py +++ b/aurweb/ratelimit.py @@ -1,11 +1,12 @@ from fastapi import Request from redis.client import Pipeline -from aurweb import config, db, logging, time +from aurweb import aur_logging, config, db, time +from aurweb.aur_redis import redis_connection from aurweb.models import ApiRateLimit -from aurweb.redis import redis_connection +from aurweb.util import get_client_ip -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) def _update_ratelimit_redis(request: Request, pipeline: Pipeline): @@ -13,7 +14,7 @@ def _update_ratelimit_redis(request: Request, pipeline: Pipeline): now = time.utcnow() time_to_delete = now - window_length - host = request.client.host + host = get_client_ip(request) window_key = f"ratelimit-ws:{host}" requests_key = f"ratelimit:{host}" @@ -38,27 +39,33 @@ def _update_ratelimit_db(request: Request): now = time.utcnow() time_to_delete = now - window_length - records = db.query(ApiRateLimit).filter( - ApiRateLimit.WindowStart < time_to_delete) - with db.begin(): - db.delete_all(records) + @db.retry_deadlock + def retry_delete(records: list[ApiRateLimit]) -> None: + with db.begin(): + db.delete_all(records) - host = request.client.host + records = db.query(ApiRateLimit).filter(ApiRateLimit.WindowStart < time_to_delete) + retry_delete(records) + + @db.retry_deadlock + def retry_create(record: ApiRateLimit, now: int, host: str) -> ApiRateLimit: + with db.begin(): + if not record: + record = db.create(ApiRateLimit, WindowStart=now, IP=host, Requests=1) + else: + record.Requests += 1 + return record + + host = get_client_ip(request) record = db.query(ApiRateLimit, ApiRateLimit.IP == host).first() - with db.begin(): - if not record: - record = db.create(ApiRateLimit, - WindowStart=now, - IP=host, Requests=1) - else: - record.Requests += 1 + record = retry_create(record, now, host) logger.debug(record.Requests) return record def update_ratelimit(request: Request, pipeline: Pipeline): - """ Update the ratelimit stored in Redis or the database depending + """Update the ratelimit stored in Redis or the database depending on AUR_CONFIG's [options] cache setting. This Redis-capable function is slightly different than most. If Redis @@ -75,7 +82,7 @@ def update_ratelimit(request: Request, pipeline: Pipeline): def check_ratelimit(request: Request): - """ Increment and check to see if request has exceeded their rate limit. + """Increment and check to see if request has exceeded their rate limit. :param request: FastAPI request :returns: True if the request host has exceeded the rate limit else False @@ -86,7 +93,7 @@ def check_ratelimit(request: Request): record = update_ratelimit(request, pipeline) # Get cache value, else None. - host = request.client.host + host = get_client_ip(request) pipeline.get(f"ratelimit:{host}") requests = pipeline.execute()[0] @@ -94,7 +101,7 @@ def check_ratelimit(request: Request): # valid cache value will be returned which must be converted # to an int. Otherwise, use the database record returned # by update_ratelimit. - if not config.getboolean("ratelimit", "cache"): + if not config.getboolean("ratelimit", "cache") or requests is None: # If we got nothing from pipeline.get, we did not use # the Redis path of logic: use the DB record's count. requests = record.Requests diff --git a/aurweb/routers/__init__.py b/aurweb/routers/__init__.py index da79e38f..552d8c28 100644 --- a/aurweb/routers/__init__.py +++ b/aurweb/routers/__init__.py @@ -3,7 +3,19 @@ API routers for FastAPI. See https://fastapi.tiangolo.com/tutorial/bigger-applications/ """ -from . import accounts, auth, html, packages, pkgbase, requests, rpc, rss, sso, trusted_user + +from . import ( + accounts, + auth, + html, + package_maintainer, + packages, + pkgbase, + requests, + rpc, + rss, + sso, +) """ aurweb application routes. This constant can be any iterable @@ -17,7 +29,7 @@ APP_ROUTES = [ packages, pkgbase, requests, - trusted_user, + package_maintainer, rss, rpc, sso, diff --git a/aurweb/routers/accounts.py b/aurweb/routers/accounts.py index b603d22a..a2d167bc 100644 --- a/aurweb/routers/accounts.py +++ b/aurweb/routers/accounts.py @@ -1,17 +1,15 @@ import copy import typing - from http import HTTPStatus -from typing import Any, Dict +from typing import Any -from fastapi import APIRouter, Form, Request +from fastapi import APIRouter, Form, HTTPException, Request from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy import and_, or_ import aurweb.config - -from aurweb import cookies, db, l10n, logging, models, util -from aurweb.auth import account_type_required, requires_auth, requires_guest +from aurweb import aur_logging, db, l10n, models, util +from aurweb.auth import account_type_required, creds, requires_auth, requires_guest from aurweb.captcha import get_captcha_salts from aurweb.exceptions import ValidationError, handle_form_exceptions from aurweb.l10n import get_translator_for_request @@ -24,7 +22,7 @@ from aurweb.users import update, validate from aurweb.users.util import get_user_by_name router = APIRouter() -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) @router.get("/passreset", response_class=HTMLResponse) @@ -34,24 +32,27 @@ async def passreset(request: Request): return render_template(request, "passreset.html", context) +@db.async_retry_deadlock @router.post("/passreset", response_class=HTMLResponse) @handle_form_exceptions @requires_guest -async def passreset_post(request: Request, - user: str = Form(...), - resetkey: str = Form(default=None), - password: str = Form(default=None), - confirm: str = Form(default=None)): +async def passreset_post( + request: Request, + user: str = Form(...), + resetkey: str = Form(default=None), + password: str = Form(default=None), + confirm: str = Form(default=None), +): context = await make_variable_context(request, "Password Reset") # The user parameter being required, we can match against criteria = or_(models.User.Username == user, models.User.Email == user) - db_user = db.query(models.User, - and_(criteria, models.User.Suspended == 0)).first() + db_user = db.query(models.User, and_(criteria, models.User.Suspended == 0)).first() if db_user is None: context["errors"] = ["Invalid e-mail."] - return render_template(request, "passreset.html", context, - status_code=HTTPStatus.NOT_FOUND) + return render_template( + request, "passreset.html", context, status_code=HTTPStatus.NOT_FOUND + ) db.refresh(db_user) if resetkey: @@ -59,29 +60,34 @@ async def passreset_post(request: Request, if not db_user.ResetKey or resetkey != db_user.ResetKey: context["errors"] = ["Invalid e-mail."] - return render_template(request, "passreset.html", context, - status_code=HTTPStatus.NOT_FOUND) + return render_template( + request, "passreset.html", context, status_code=HTTPStatus.NOT_FOUND + ) if not user or not password: context["errors"] = ["Missing a required field."] - return render_template(request, "passreset.html", context, - status_code=HTTPStatus.BAD_REQUEST) + return render_template( + request, "passreset.html", context, status_code=HTTPStatus.BAD_REQUEST + ) if password != confirm: # If the provided password does not match the provided confirm. context["errors"] = ["Password fields do not match."] - return render_template(request, "passreset.html", context, - status_code=HTTPStatus.BAD_REQUEST) + return render_template( + request, "passreset.html", context, status_code=HTTPStatus.BAD_REQUEST + ) if len(password) < models.User.minimum_passwd_length(): # Translate the error here, which simplifies error output # in the jinja2 template. _ = get_translator_for_request(request) - context["errors"] = [_( - "Your password must be at least %s characters.") % ( - str(models.User.minimum_passwd_length()))] - return render_template(request, "passreset.html", context, - status_code=HTTPStatus.BAD_REQUEST) + context["errors"] = [ + _("Your password must be at least %s characters.") + % (str(models.User.minimum_passwd_length())) + ] + return render_template( + request, "passreset.html", context, status_code=HTTPStatus.BAD_REQUEST + ) # We got to this point; everything matched up. Update the password # and remove the ResetKey. @@ -92,8 +98,9 @@ async def passreset_post(request: Request, db_user.update_password(password) # Render ?step=complete. - return RedirectResponse(url="/passreset?step=complete", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse( + url="/passreset?step=complete", status_code=HTTPStatus.SEE_OTHER + ) # If we got here, we continue with issuing a resetkey for the user. resetkey = generate_resetkey() @@ -103,13 +110,13 @@ async def passreset_post(request: Request, ResetKeyNotification(db_user.ID).send() # Render ?step=confirm. - return RedirectResponse(url="/passreset?step=confirm", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse( + url="/passreset?step=confirm", status_code=HTTPStatus.SEE_OTHER + ) -def process_account_form(request: Request, user: models.User, - args: Dict[str, Any]): - """ Process an account form. All fields are optional and only checks +def process_account_form(request: Request, user: models.User, args: dict[str, Any]): + """Process an account form. All fields are optional and only checks requirements in the case they are present. ``` @@ -146,23 +153,22 @@ def process_account_form(request: Request, user: models.User, validate.username_in_use, validate.email_in_use, validate.invalid_account_type, - validate.invalid_captcha + validate.invalid_captcha, ] try: for check in checks: check(**args, request=request, user=user, _=_) except ValidationError as exc: - return (False, exc.data) + return False, exc.data - return (True, []) + return True, [] -def make_account_form_context(context: dict, - request: Request, - user: models.User, - args: dict): - """ Modify a FastAPI context and add attributes for the account form. +def make_account_form_context( + context: dict, request: Request, user: models.User, args: dict +): + """Modify a FastAPI context and add attributes for the account form. :param context: FastAPI context :param request: FastAPI request @@ -173,15 +179,17 @@ def make_account_form_context(context: dict, # Do not modify the original context. context = copy.copy(context) - context["account_types"] = list(filter( - lambda e: request.user.AccountTypeID >= e[0], - [ - (at.USER_ID, f"Normal {at.USER}"), - (at.TRUSTED_USER_ID, at.TRUSTED_USER), - (at.DEVELOPER_ID, at.DEVELOPER), - (at.TRUSTED_USER_AND_DEV_ID, at.TRUSTED_USER_AND_DEV) - ] - )) + context["account_types"] = list( + filter( + lambda e: request.user.AccountTypeID >= e[0], + [ + (at.USER_ID, f"Normal {at.USER}"), + (at.PACKAGE_MAINTAINER_ID, at.PACKAGE_MAINTAINER), + (at.DEVELOPER_ID, at.DEVELOPER), + (at.PACKAGE_MAINTAINER_AND_DEV_ID, at.PACKAGE_MAINTAINER_AND_DEV), + ], + ) + ) if request.user.is_authenticated(): context["username"] = args.get("U", user.Username) @@ -201,6 +209,7 @@ def make_account_form_context(context: dict, context["cn"] = args.get("CN", user.CommentNotify) context["un"] = args.get("UN", user.UpdateNotify) context["on"] = args.get("ON", user.OwnershipNotify) + context["hdc"] = args.get("HDC", user.HideDeletedComments) context["inactive"] = args.get("J", user.InactivityTS != 0) else: context["username"] = args.get("U", str()) @@ -219,6 +228,7 @@ def make_account_form_context(context: dict, context["cn"] = args.get("CN", True) context["un"] = args.get("UN", False) context["on"] = args.get("ON", True) + context["hdc"] = args.get("HDC", False) context["inactive"] = args.get("J", False) context["password"] = args.get("P", str()) @@ -229,59 +239,62 @@ def make_account_form_context(context: dict, @router.get("/register", response_class=HTMLResponse) @requires_guest -async def account_register(request: Request, - U: str = Form(default=str()), # Username - E: str = Form(default=str()), # Email - H: str = Form(default=False), # Hide Email - BE: str = Form(default=None), # Backup Email - R: str = Form(default=None), # Real Name - HP: str = Form(default=None), # Homepage - I: str = Form(default=None), # IRC Nick - K: str = Form(default=None), # PGP Key FP - L: str = Form(default=aurweb.config.get( - "options", "default_lang")), - TZ: str = Form(default=aurweb.config.get( - "options", "default_timezone")), - PK: str = Form(default=None), - CN: bool = Form(default=False), # Comment Notify - CU: bool = Form(default=False), # Update Notify - CO: bool = Form(default=False), # Owner Notify - captcha: str = Form(default=str())): +async def account_register( + request: Request, + U: str = Form(default=str()), # Username + E: str = Form(default=str()), # Email + H: str = Form(default=False), # Hide Email + BE: str = Form(default=None), # Backup Email + R: str = Form(default=None), # Real Name + HP: str = Form(default=None), # Homepage + I: str = Form(default=None), # IRC Nick + K: str = Form(default=None), # PGP Key FP + L: str = Form(default=aurweb.config.get("options", "default_lang")), + TZ: str = Form(default=aurweb.config.get("options", "default_timezone")), + PK: str = Form(default=None), + CN: bool = Form(default=False), # Comment Notify + CU: bool = Form(default=False), # Update Notify + CO: bool = Form(default=False), # Owner Notify + HDC: bool = Form(default=False), # Hide Deleted Comments + captcha: str = Form(default=str()), +): context = await make_variable_context(request, "Register") context["captcha_salt"] = get_captcha_salts()[0] context = make_account_form_context(context, request, None, dict()) return render_template(request, "register.html", context) +@db.async_retry_deadlock @router.post("/register", response_class=HTMLResponse) @handle_form_exceptions @requires_guest -async def account_register_post(request: Request, - U: str = Form(default=str()), # Username - E: str = Form(default=str()), # Email - H: str = Form(default=False), # Hide Email - BE: str = Form(default=None), # Backup Email - R: str = Form(default=''), # Real Name - HP: str = Form(default=None), # Homepage - I: str = Form(default=None), # IRC Nick - K: str = Form(default=None), # PGP Key - L: str = Form(default=aurweb.config.get( - "options", "default_lang")), - TZ: str = Form(default=aurweb.config.get( - "options", "default_timezone")), - PK: str = Form(default=str()), # SSH PubKey - CN: bool = Form(default=False), - UN: bool = Form(default=False), - ON: bool = Form(default=False), - captcha: str = Form(default=None), - captcha_salt: str = Form(...)): +async def account_register_post( + request: Request, + U: str = Form(default=str()), # Username + E: str = Form(default=str()), # Email + H: str = Form(default=False), # Hide Email + BE: str = Form(default=None), # Backup Email + R: str = Form(default=""), # Real Name + HP: str = Form(default=None), # Homepage + I: str = Form(default=None), # IRC Nick + K: str = Form(default=None), # PGP Key + L: str = Form(default=aurweb.config.get("options", "default_lang")), + TZ: str = Form(default=aurweb.config.get("options", "default_timezone")), + PK: str = Form(default=str()), # SSH PubKey + CN: bool = Form(default=False), + UN: bool = Form(default=False), + ON: bool = Form(default=False), + HDC: bool = Form(default=False), + captcha: str = Form(default=None), + captcha_salt: str = Form(...), +): context = await make_variable_context(request, "Register") args = dict(await request.form()) args["K"] = args.get("K", str()).replace(" ", "") K = args.get("K") # Force "H" into a boolean. - args["H"] = H = (args.get("H", str()) == "on") + args["H"] = H = args.get("H", str()) == "on" context = make_account_form_context(context, request, None, args) ok, errors = process_account_form(request, request.user, args) @@ -289,42 +302,56 @@ async def account_register_post(request: Request, # If the field values given do not meet the requirements, # return HTTP 400 with an error. context["errors"] = errors - return render_template(request, "register.html", context, - status_code=HTTPStatus.BAD_REQUEST) + return render_template( + request, "register.html", context, status_code=HTTPStatus.BAD_REQUEST + ) if not captcha: context["errors"] = ["The CAPTCHA is missing."] - return render_template(request, "register.html", context, - status_code=HTTPStatus.BAD_REQUEST) + return render_template( + request, "register.html", context, status_code=HTTPStatus.BAD_REQUEST + ) # Create a user with no password with a resetkey, then send # an email off about it. resetkey = generate_resetkey() # By default, we grab the User account type to associate with. - atype = db.query(models.AccountType, - models.AccountType.AccountType == "User").first() + atype = db.query( + models.AccountType, models.AccountType.AccountType == "User" + ).first() # Create a user given all parameters available. with db.begin(): - user = db.create(models.User, Username=U, - Email=E, HideEmail=H, BackupEmail=BE, - RealName=R, Homepage=HP, IRCNick=I, PGPKey=K, - LangPreference=L, Timezone=TZ, CommentNotify=CN, - UpdateNotify=UN, OwnershipNotify=ON, - ResetKey=resetkey, AccountType=atype) + user = db.create( + models.User, + Username=U, + Email=E, + HideEmail=H, + BackupEmail=BE, + RealName=R, + Homepage=HP, + IRCNick=I, + PGPKey=K, + LangPreference=L, + Timezone=TZ, + CommentNotify=CN, + UpdateNotify=UN, + OwnershipNotify=ON, + HideDeletedComments=HDC, + ResetKey=resetkey, + AccountType=atype, + ) - # If a PK was given and either one does not exist or the given - # PK mismatches the existing user's SSHPubKey.PubKey. - if PK: - # Get the second element in the PK, which is the actual key. - keys = util.parse_ssh_keys(PK.strip()) - for k in keys: - pk = " ".join(k) - fprint = get_fingerprint(pk) - with db.begin(): - db.create(models.SSHPubKey, UserID=user.ID, - PubKey=pk, Fingerprint=fprint) + # If a PK was given and either one does not exist or the given + # PK mismatches the existing user's SSHPubKey.PubKey. + if PK: + # Get the second element in the PK, which is the actual key. + keys = util.parse_ssh_keys(PK.strip()) + for k in keys: + pk = " ".join(k) + fprint = get_fingerprint(pk) + db.create(models.SSHPubKey, User=user, PubKey=pk, Fingerprint=fprint) # Send a reset key notification to the new user. WelcomeNotification(user.ID).send() @@ -334,8 +361,9 @@ async def account_register_post(request: Request, return render_template(request, "register.html", context) -def cannot_edit(request: Request, user: models.User) \ - -> typing.Optional[RedirectResponse]: +def cannot_edit( + request: Request, user: models.User +) -> typing.Optional[RedirectResponse]: """ Decide if `request.user` cannot edit `user`. @@ -346,6 +374,9 @@ def cannot_edit(request: Request, user: models.User) \ :param user: Target user to be edited :return: RedirectResponse if approval != granted else None """ + # raise 404 if user does not exist + if not user: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND) approved = request.user.can_edit_user(user) if not approved and (to := "/"): if user: @@ -373,31 +404,32 @@ async def account_edit(request: Request, username: str): @router.post("/account/{username}/edit", response_class=HTMLResponse) @handle_form_exceptions @requires_auth -async def account_edit_post(request: Request, - username: str, - U: str = Form(default=str()), # Username - J: bool = Form(default=False), - E: str = Form(default=str()), # Email - H: str = Form(default=False), # Hide Email - BE: str = Form(default=None), # Backup Email - R: str = Form(default=None), # Real Name - HP: str = Form(default=None), # Homepage - I: str = Form(default=None), # IRC Nick - K: str = Form(default=None), # PGP Key - L: str = Form(aurweb.config.get( - "options", "default_lang")), - TZ: str = Form(aurweb.config.get( - "options", "default_timezone")), - P: str = Form(default=str()), # New Password - C: str = Form(default=None), # Password Confirm - PK: str = Form(default=None), # PubKey - CN: bool = Form(default=False), # Comment Notify - UN: bool = Form(default=False), # Update Notify - ON: bool = Form(default=False), # Owner Notify - T: int = Form(default=None), - passwd: str = Form(default=str())): - user = db.query(models.User).filter( - models.User.Username == username).first() +async def account_edit_post( + request: Request, + username: str, + U: str = Form(default=str()), # Username + J: bool = Form(default=False), + E: str = Form(default=str()), # Email + H: str = Form(default=False), # Hide Email + BE: str = Form(default=None), # Backup Email + R: str = Form(default=None), # Real Name + HP: str = Form(default=None), # Homepage + I: str = Form(default=None), # IRC Nick + K: str = Form(default=None), # PGP Key + L: str = Form(aurweb.config.get("options", "default_lang")), + TZ: str = Form(aurweb.config.get("options", "default_timezone")), + P: str = Form(default=str()), # New Password + C: str = Form(default=None), # Password Confirm + S: bool = Form(default=False), # Suspended + PK: str = Form(default=None), # PubKey + CN: bool = Form(default=False), # Comment Notify + UN: bool = Form(default=False), # Update Notify + ON: bool = Form(default=False), # Owner Notify + HDC: bool = Form(default=False), # Hide Deleted Comments + T: int = Form(default=None), + passwd: str = Form(default=str()), +): + user = db.query(models.User).filter(models.User.Username == username).first() response = cannot_edit(request, user) if response: return response @@ -416,13 +448,15 @@ async def account_edit_post(request: Request, if not passwd: context["errors"] = ["Invalid password."] - return render_template(request, "account/edit.html", context, - status_code=HTTPStatus.BAD_REQUEST) + return render_template( + request, "account/edit.html", context, status_code=HTTPStatus.BAD_REQUEST + ) if not ok: context["errors"] = errors - return render_template(request, "account/edit.html", context, - status_code=HTTPStatus.BAD_REQUEST) + return render_template( + request, "account/edit.html", context, status_code=HTTPStatus.BAD_REQUEST + ) updates = [ update.simple, @@ -430,29 +464,29 @@ async def account_edit_post(request: Request, update.timezone, update.ssh_pubkey, update.account_type, - update.password + update.password, + update.suspend, ] + # These update functions are all guarded by retry_deadlock; + # there's no need to guard this route itself. for f in updates: f(**args, request=request, user=user, context=context) if not errors: context["complete"] = True - # Update cookies with requests, in case they were changed. - response = render_template(request, "account/edit.html", context) - return cookies.update_response_cookies(request, response, - aurtz=TZ, aurlang=L) + return render_template(request, "account/edit.html", context) @router.get("/account/{username}") async def account(request: Request, username: str): _ = l10n.get_translator_for_request(request) - context = await make_variable_context( - request, _("Account") + " " + username) + context = await make_variable_context(request, _("Account") + " " + username) if not request.user.is_authenticated(): - return render_template(request, "account/show.html", context, - status_code=HTTPStatus.UNAUTHORIZED) + return render_template( + request, "account/show.html", context, status_code=HTTPStatus.UNAUTHORIZED + ) # Get related User record, if possible. user = get_user_by_name(username) @@ -460,11 +494,10 @@ async def account(request: Request, username: str): # Format PGPKey for display with a space between each 4 characters. k = user.PGPKey or str() - context["pgp_key"] = " ".join([k[i:i + 4] for i in range(0, len(k), 4)]) + context["pgp_key"] = " ".join([k[i : i + 4] for i in range(0, len(k), 4)]) login_ts = None - session = db.query(models.Session).filter( - models.Session.UsersID == user.ID).first() + session = db.query(models.Session).filter(models.Session.UsersID == user.ID).first() if session: login_ts = user.session.LastUpdateTS context["login_ts"] = login_ts @@ -480,15 +513,16 @@ async def account_comments(request: Request, username: str): context = make_context(request, "Accounts") context["username"] = username context["comments"] = user.package_comments.order_by( - models.PackageComment.CommentTS.desc()) + models.PackageComment.CommentTS.desc() + ) return render_template(request, "account/comments.html", context) @router.get("/accounts") @requires_auth -@account_type_required({at.TRUSTED_USER, - at.DEVELOPER, - at.TRUSTED_USER_AND_DEV}) +@account_type_required( + {at.PACKAGE_MAINTAINER, at.DEVELOPER, at.PACKAGE_MAINTAINER_AND_DEV} +) async def accounts(request: Request): context = make_context(request, "Accounts") return render_template(request, "account/search.html", context) @@ -497,19 +531,21 @@ async def accounts(request: Request): @router.post("/accounts") @handle_form_exceptions @requires_auth -@account_type_required({at.TRUSTED_USER, - at.DEVELOPER, - at.TRUSTED_USER_AND_DEV}) -async def accounts_post(request: Request, - O: int = Form(default=0), # Offset - SB: str = Form(default=str()), # Sort By - U: str = Form(default=str()), # Username - T: str = Form(default=str()), # Account Type - S: bool = Form(default=False), # Suspended - E: str = Form(default=str()), # Email - R: str = Form(default=str()), # Real Name - I: str = Form(default=str()), # IRC Nick - K: str = Form(default=str())): # PGP Key +@account_type_required( + {at.PACKAGE_MAINTAINER, at.DEVELOPER, at.PACKAGE_MAINTAINER_AND_DEV} +) +async def accounts_post( + request: Request, + O: int = Form(default=0), # Offset + SB: str = Form(default=str()), # Sort By + U: str = Form(default=str()), # Username + T: str = Form(default=str()), # Account Type + S: bool = Form(default=False), # Suspended + E: str = Form(default=str()), # Email + R: str = Form(default=str()), # Real Name + I: str = Form(default=str()), # IRC Nick + K: str = Form(default=str()), +): # PGP Key context = await make_variable_context(request, "Accounts") context["pp"] = pp = 50 # Hits per page. @@ -532,9 +568,9 @@ async def accounts_post(request: Request, # Convert parameter T to an AccountType ID. account_types = { "u": at.USER_ID, - "t": at.TRUSTED_USER_ID, + "t": at.PACKAGE_MAINTAINER_ID, "d": at.DEVELOPER_ID, - "td": at.TRUSTED_USER_AND_DEV_ID + "td": at.PACKAGE_MAINTAINER_AND_DEV_ID, } account_type_id = account_types.get(T, None) @@ -545,7 +581,8 @@ async def accounts_post(request: Request, # Populate this list with any additional statements to # be ANDed together. statements = [ - v for k, v in [ + v + for k, v in [ (account_type_id is not None, models.AccountType.ID == account_type_id), (bool(U), models.User.Username.like(f"%{U}%")), (bool(S), models.User.Suspended == S), @@ -553,7 +590,8 @@ async def accounts_post(request: Request, (bool(R), models.User.RealName.like(f"%{R}%")), (bool(I), models.User.IRCNick.like(f"%{I}%")), (bool(K), models.User.PGPKey.like(f"%{K}%")), - ] if k + ] + if k ] # Filter the query by coe-mbining all statements added above into @@ -571,9 +609,79 @@ async def accounts_post(request: Request, return render_template(request, "account/index.html", context) -def render_terms_of_service(request: Request, - context: dict, - terms: typing.Iterable): +@router.get("/account/{name}/delete") +@requires_auth +async def account_delete(request: Request, name: str): + user = db.query(models.User).filter(models.User.Username == name).first() + if not user: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND) + + has_cred = request.user.has_credential(creds.ACCOUNT_EDIT, approved=[user]) + if not has_cred: + _ = l10n.get_translator_for_request(request) + raise HTTPException( + detail=_("You do not have permission to edit this account."), + status_code=HTTPStatus.UNAUTHORIZED, + ) + + context = make_context(request, "Accounts") + context["name"] = name + return render_template(request, "account/delete.html", context) + + +@db.async_retry_deadlock +@router.post("/account/{name}/delete") +@handle_form_exceptions +@requires_auth +async def account_delete_post( + request: Request, + name: str, + passwd: str = Form(default=str()), + confirm: bool = Form(default=False), +): + user = db.query(models.User).filter(models.User.Username == name).first() + if not user: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND) + + has_cred = request.user.has_credential(creds.ACCOUNT_EDIT, approved=[user]) + if not has_cred: + _ = l10n.get_translator_for_request(request) + raise HTTPException( + detail=_("You do not have permission to edit this account."), + status_code=HTTPStatus.UNAUTHORIZED, + ) + + context = make_context(request, "Accounts") + context["name"] = name + + confirm = util.strtobool(confirm) + if not confirm: + context["errors"] = [ + "The account has not been deleted, check the confirmation checkbox." + ] + return render_template( + request, + "account/delete.html", + context, + status_code=HTTPStatus.BAD_REQUEST, + ) + + if not request.user.valid_password(passwd): + context["errors"] = ["Invalid password."] + return render_template( + request, + "account/delete.html", + context, + status_code=HTTPStatus.BAD_REQUEST, + ) + + with db.begin(): + db.delete(user) + + return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER) + + +def render_terms_of_service(request: Request, context: dict, terms: typing.Iterable): if not terms: return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER) context["unaccepted_terms"] = terms @@ -585,14 +693,21 @@ def render_terms_of_service(request: Request, async def terms_of_service(request: Request): # Query the database for terms that were previously accepted, # but now have a bumped Revision that needs to be accepted. - diffs = db.query(models.Term).join(models.AcceptedTerm).filter( - models.AcceptedTerm.Revision < models.Term.Revision).all() + diffs = ( + db.query(models.Term) + .join(models.AcceptedTerm) + .filter(models.AcceptedTerm.Revision < models.Term.Revision) + .all() + ) # Query the database for any terms that have not yet been accepted. - unaccepted = db.query(models.Term).filter( - ~models.Term.ID.in_(db.query(models.AcceptedTerm.TermsID))).all() + unaccepted = ( + db.query(models.Term) + .filter(~models.Term.ID.in_(db.query(models.AcceptedTerm.TermsID))) + .all() + ) - for record in (diffs + unaccepted): + for record in diffs + unaccepted: db.refresh(record) # Translate the 'Terms of Service' part of our page title. @@ -604,19 +719,26 @@ async def terms_of_service(request: Request): return render_terms_of_service(request, context, accept_needed) +@db.async_retry_deadlock @router.post("/tos") @handle_form_exceptions @requires_auth -async def terms_of_service_post(request: Request, - accept: bool = Form(default=False)): +async def terms_of_service_post(request: Request, accept: bool = Form(default=False)): # Query the database for terms that were previously accepted, # but now have a bumped Revision that needs to be accepted. - diffs = db.query(models.Term).join(models.AcceptedTerm).filter( - models.AcceptedTerm.Revision < models.Term.Revision).all() + diffs = ( + db.query(models.Term) + .join(models.AcceptedTerm) + .filter(models.AcceptedTerm.Revision < models.Term.Revision) + .all() + ) # Query the database for any terms that have not yet been accepted. - unaccepted = db.query(models.Term).filter( - ~models.Term.ID.in_(db.query(models.AcceptedTerm.TermsID))).all() + unaccepted = ( + db.query(models.Term) + .filter(~models.Term.ID.in_(db.query(models.AcceptedTerm.TermsID))) + .all() + ) if not accept: # Translate the 'Terms of Service' part of our page title. @@ -628,7 +750,8 @@ async def terms_of_service_post(request: Request, # them instead of reiterating the process in terms_of_service. accept_needed = sorted(unaccepted + diffs) return render_terms_of_service( - request, context, util.apply_all(accept_needed, db.refresh)) + request, context, util.apply_all(accept_needed, db.refresh) + ) with db.begin(): # For each term we found, query for the matching accepted term @@ -636,13 +759,18 @@ async def terms_of_service_post(request: Request, for term in diffs: db.refresh(term) accepted_term = request.user.accepted_terms.filter( - models.AcceptedTerm.TermsID == term.ID).first() + models.AcceptedTerm.TermsID == term.ID + ).first() accepted_term.Revision = term.Revision # For each term that was never accepted, accept it! for term in unaccepted: db.refresh(term) - db.create(models.AcceptedTerm, User=request.user, - Term=term, Revision=term.Revision) + db.create( + models.AcceptedTerm, + User=request.user, + Term=term, + Revision=term.Revision, + ) return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER) diff --git a/aurweb/routers/auth.py b/aurweb/routers/auth.py index 9f465388..88eaa0e6 100644 --- a/aurweb/routers/auth.py +++ b/aurweb/routers/auth.py @@ -5,8 +5,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy import or_ import aurweb.config - -from aurweb import cookies, db, time +from aurweb import cookies, db from aurweb.auth import requires_auth, requires_guest from aurweb.exceptions import handle_form_exceptions from aurweb.l10n import get_translator_for_request @@ -17,7 +16,7 @@ router = APIRouter() async def login_template(request: Request, next: str, errors: list = None): - """ Provide login-specific template context to render_template. """ + """Provide login-specific template context to render_template.""" context = await make_variable_context(request, "Login", next) context["errors"] = errors context["url_base"] = f"{request.url.scheme}://{request.url.netloc}" @@ -29,77 +28,95 @@ async def login_get(request: Request, next: str = "/"): return await login_template(request, next) +@db.retry_deadlock +def _retry_login(request: Request, user: User, passwd: str) -> str: + return user.login(request, passwd) + + @router.post("/login", response_class=HTMLResponse) @handle_form_exceptions @requires_guest -async def login_post(request: Request, - next: str = Form(...), - user: str = Form(default=str()), - passwd: str = Form(default=str()), - remember_me: bool = Form(default=False)): +async def login_post( + request: Request, + next: str = Form(...), + user: str = Form(default=str()), + passwd: str = Form(default=str()), + remember_me: bool = Form(default=False), +): # TODO: Once the Origin header gets broader adoption, this code can be # slightly simplified to use it. login_path = aurweb.config.get("options", "aur_location") + "/login" referer = request.headers.get("Referer") if not referer or not referer.startswith(login_path): _ = get_translator_for_request(request) - raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, - detail=_("Bad Referer header.")) - - with db.begin(): - user = db.query(User).filter( - or_(User.Username == user, User.Email == user) - ).first() + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, detail=_("Bad Referer header.") + ) + user = ( + db.query(User) + .filter( + or_( + User.Username == user, + User.Email == user, + ) + ) + .first() + ) if not user: - return await login_template(request, next, - errors=["Bad username or password."]) + return await login_template(request, next, errors=["Bad username or password."]) if user.Suspended: - return await login_template(request, next, - errors=["Account Suspended"]) + return await login_template(request, next, errors=["Account Suspended"]) - cookie_timeout = cookies.timeout(remember_me) - sid = user.login(request, passwd, cookie_timeout) + # If "remember me" was not ticked, we set a session cookie for AURSID, + # otherwise we make it a persistent cookie + cookie_timeout = None + if remember_me: + cookie_timeout = aurweb.config.getint("options", "persistent_cookie_timeout") + + perma_timeout = aurweb.config.getint("options", "permanent_cookie_timeout") + sid = _retry_login(request, user, passwd) if not sid: - return await login_template(request, next, - errors=["Bad username or password."]) + return await login_template(request, next, errors=["Bad username or password."]) - login_timeout = aurweb.config.getint("options", "login_timeout") - - expires_at = int(time.utcnow() + max(cookie_timeout, login_timeout)) - - response = RedirectResponse(url=next, - status_code=HTTPStatus.SEE_OTHER) + response = RedirectResponse(url=next, status_code=HTTPStatus.SEE_OTHER) secure = aurweb.config.getboolean("options", "disable_http_login") - response.set_cookie("AURSID", sid, expires=expires_at, - secure=secure, httponly=secure, - samesite=cookies.samesite()) - response.set_cookie("AURTZ", user.Timezone, - secure=secure, httponly=secure, - samesite=cookies.samesite()) - response.set_cookie("AURLANG", user.LangPreference, - secure=secure, httponly=secure, - samesite=cookies.samesite()) - response.set_cookie("AURREMEMBER", remember_me, - expires=expires_at, - secure=secure, httponly=secure, - samesite=cookies.samesite()) + response.set_cookie( + "AURSID", + sid, + max_age=cookie_timeout, + secure=secure, + httponly=secure, + samesite=cookies.samesite(), + ) + response.set_cookie( + "AURREMEMBER", + remember_me, + max_age=perma_timeout, + secure=secure, + httponly=secure, + samesite=cookies.samesite(), + ) return response +@db.retry_deadlock +def _retry_logout(request: Request) -> None: + request.user.logout(request) + + @router.post("/logout") @handle_form_exceptions @requires_auth async def logout(request: Request, next: str = Form(default="/")): if request.user.is_authenticated(): - request.user.logout(request) + _retry_logout(request) # Use 303 since we may be handling a post request, that'll get it # to redirect to a get request. - response = RedirectResponse(url=next, - status_code=HTTPStatus.SEE_OTHER) + response = RedirectResponse(url=next, status_code=HTTPStatus.SEE_OTHER) response.delete_cookie("AURSID") - response.delete_cookie("AURTZ") + response.delete_cookie("AURREMEMBER") return response diff --git a/aurweb/routers/html.py b/aurweb/routers/html.py index d31a32c7..25c611d0 100644 --- a/aurweb/routers/html.py +++ b/aurweb/routers/html.py @@ -1,43 +1,48 @@ """ AURWeb's primary routing module. Define all routes via @app.app.{get,post} decorators in some way; more complex routes should be defined in their own modules and imported here. """ -import os +import os from http import HTTPStatus from fastapi import APIRouter, Form, HTTPException, Request, Response from fastapi.responses import HTMLResponse, RedirectResponse -from prometheus_client import CONTENT_TYPE_LATEST, CollectorRegistry, generate_latest, multiprocess -from sqlalchemy import and_, case, or_ +from prometheus_client import ( + CONTENT_TYPE_LATEST, + CollectorRegistry, + generate_latest, + multiprocess, +) +from sqlalchemy import case, or_ import aurweb.config import aurweb.models.package_request - -from aurweb import cookies, db, logging, models, time, util -from aurweb.cache import db_count_cache +from aurweb import aur_logging, cookies, db, models, statistics, time, util from aurweb.exceptions import handle_form_exceptions -from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID from aurweb.models.package_request import PENDING_ID from aurweb.packages.util import query_notified, query_voted, updated_packages from aurweb.templates import make_context, render_template -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) router = APIRouter() @router.get("/favicon.ico") async def favicon(request: Request): - """ Some browsers attempt to find a website's favicon via root uri at - /favicon.ico, so provide a redirection here to our static icon. """ + """Some browsers attempt to find a website's favicon via root uri at + /favicon.ico, so provide a redirection here to our static icon.""" return RedirectResponse("/static/images/favicon.ico") +@db.async_retry_deadlock @router.post("/language", response_class=RedirectResponse) @handle_form_exceptions -async def language(request: Request, - set_lang: str = Form(...), - next: str = Form(...), - q: str = Form(default=None)): +async def language( + request: Request, + set_lang: str = Form(...), + next: str = Form(...), + q: str = Form(default=None), +): """ A POST route used to set a session's language. @@ -45,92 +50,48 @@ async def language(request: Request, setting the language on any page, we want to preserve query parameters across the redirect. """ - if next[0] != '/': + if next[0] != "/": return HTMLResponse(b"Invalid 'next' parameter.", status_code=400) query_string = "?" + q if q else str() + response = RedirectResponse( + url=f"{next}{query_string}", status_code=HTTPStatus.SEE_OTHER + ) + # If the user is authenticated, update the user's LangPreference. + # Otherwise set an AURLANG cookie if request.user.is_authenticated(): with db.begin(): request.user.LangPreference = set_lang + else: + secure = aurweb.config.getboolean("options", "disable_http_login") + perma_timeout = aurweb.config.getint("options", "permanent_cookie_timeout") + + response.set_cookie( + "AURLANG", + set_lang, + secure=secure, + httponly=secure, + max_age=perma_timeout, + samesite=cookies.samesite(), + ) - # In any case, set the response's AURLANG cookie that never expires. - response = RedirectResponse(url=f"{next}{query_string}", - status_code=HTTPStatus.SEE_OTHER) - secure = aurweb.config.getboolean("options", "disable_http_login") - response.set_cookie("AURLANG", set_lang, - secure=secure, httponly=secure, - samesite=cookies.samesite()) return response @router.get("/", response_class=HTMLResponse) async def index(request: Request): - """ Homepage route. """ + """Homepage route.""" context = make_context(request, "Home") - context['ssh_fingerprints'] = util.get_ssh_fingerprints() + context["ssh_fingerprints"] = util.get_ssh_fingerprints() - bases = db.query(models.PackageBase) - - redis = aurweb.redis.redis_connection() - cache_expire = 300 # Five minutes. + cache_expire = aurweb.config.getint("cache", "expiry_time_statistics", 300) # Package statistics. - query = bases.filter(models.PackageBase.PackagerUID.isnot(None)) - context["package_count"] = await db_count_cache( - redis, "package_count", query, expire=cache_expire) - - query = bases.filter( - and_(models.PackageBase.MaintainerUID.is_(None), - models.PackageBase.PackagerUID.isnot(None)) - ) - context["orphan_count"] = await db_count_cache( - redis, "orphan_count", query, expire=cache_expire) - - query = db.query(models.User) - context["user_count"] = await db_count_cache( - redis, "user_count", query, expire=cache_expire) - - query = query.filter( - or_(models.User.AccountTypeID == TRUSTED_USER_ID, - models.User.AccountTypeID == TRUSTED_USER_AND_DEV_ID)) - context["trusted_user_count"] = await db_count_cache( - redis, "trusted_user_count", query, expire=cache_expire) - - # Current timestamp. - now = time.utcnow() - - seven_days = 86400 * 7 # Seven days worth of seconds. - seven_days_ago = now - seven_days - - one_hour = 3600 - updated = bases.filter( - and_(models.PackageBase.ModifiedTS - models.PackageBase.SubmittedTS >= one_hour, - models.PackageBase.PackagerUID.isnot(None)) - ) - - query = bases.filter( - and_(models.PackageBase.SubmittedTS >= seven_days_ago, - models.PackageBase.PackagerUID.isnot(None)) - ) - context["seven_days_old_added"] = await db_count_cache( - redis, "seven_days_old_added", query, expire=cache_expire) - - query = updated.filter(models.PackageBase.ModifiedTS >= seven_days_ago) - context["seven_days_old_updated"] = await db_count_cache( - redis, "seven_days_old_updated", query, expire=cache_expire) - - year = seven_days * 52 # Fifty two weeks worth: one year. - year_ago = now - year - query = updated.filter(models.PackageBase.ModifiedTS >= year_ago) - context["year_old_updated"] = await db_count_cache( - redis, "year_old_updated", query, expire=cache_expire) - - query = bases.filter( - models.PackageBase.ModifiedTS - models.PackageBase.SubmittedTS < 3600) - context["never_updated"] = await db_count_cache( - redis, "never_updated", query, expire=cache_expire) + counts = statistics.get_homepage_counts() + for k in counts: + context[k] = counts[k] # Get the 15 most recently updated packages. context["package_updates"] = updated_packages(15, cache_expire) @@ -140,78 +101,92 @@ async def index(request: Request): # the dashboard display. packages = db.query(models.Package).join(models.PackageBase) - maintained = packages.join( - models.PackageComaintainer, - models.PackageComaintainer.PackageBaseID == models.PackageBase.ID, - isouter=True - ).join( - models.User, - or_(models.PackageBase.MaintainerUID == models.User.ID, - models.PackageComaintainer.UsersID == models.User.ID) - ).filter( - models.User.ID == request.user.ID + maintained = ( + packages.join( + models.PackageComaintainer, + models.PackageComaintainer.PackageBaseID == models.PackageBase.ID, + isouter=True, + ) + .join( + models.User, + or_( + models.PackageBase.MaintainerUID == models.User.ID, + models.PackageComaintainer.UsersID == models.User.ID, + ), + ) + .filter(models.User.ID == request.user.ID) ) # Packages maintained by the user that have been flagged. - context["flagged_packages"] = maintained.filter( - models.PackageBase.OutOfDateTS.isnot(None) - ).order_by( - models.PackageBase.ModifiedTS.desc(), models.Package.Name.asc() - ).limit(50).all() + context["flagged_packages"] = ( + maintained.filter(models.PackageBase.OutOfDateTS.isnot(None)) + .order_by(models.PackageBase.ModifiedTS.desc(), models.Package.Name.asc()) + .limit(50) + .all() + ) # Flagged packages that request.user has voted for. context["flagged_packages_voted"] = query_voted( - context.get("flagged_packages"), request.user) + context.get("flagged_packages"), request.user + ) # Flagged packages that request.user is being notified about. context["flagged_packages_notified"] = query_notified( - context.get("flagged_packages"), request.user) + context.get("flagged_packages"), request.user + ) - archive_time = aurweb.config.getint('options', 'request_archive_time') - start = now - archive_time + archive_time = aurweb.config.getint("options", "request_archive_time") + start = time.utcnow() - archive_time # Package requests created by request.user. - context["package_requests"] = request.user.package_requests.filter( - models.PackageRequest.RequestTS >= start - ).order_by( - # Order primarily by the Status column being PENDING_ID, - # and secondarily by RequestTS; both in descending order. - case([(models.PackageRequest.Status == PENDING_ID, 1)], - else_=0).desc(), - models.PackageRequest.RequestTS.desc() - ).limit(50).all() + context["package_requests"] = ( + request.user.package_requests.filter( + models.PackageRequest.RequestTS >= start + ) + .order_by( + # Order primarily by the Status column being PENDING_ID, + # and secondarily by RequestTS; both in descending order. + case([(models.PackageRequest.Status == PENDING_ID, 1)], else_=0).desc(), + models.PackageRequest.RequestTS.desc(), + ) + .limit(50) + .all() + ) # Packages that the request user maintains or comaintains. - context["packages"] = maintained.filter( - models.User.ID == models.PackageBase.MaintainerUID - ).order_by( - models.PackageBase.ModifiedTS.desc(), models.Package.Name.desc() - ).limit(50).all() + context["packages"] = ( + maintained.filter(models.User.ID == models.PackageBase.MaintainerUID) + .order_by(models.PackageBase.ModifiedTS.desc(), models.Package.Name.desc()) + .limit(50) + .all() + ) # Packages that request.user has voted for. - context["packages_voted"] = query_voted( - context.get("packages"), request.user) + context["packages_voted"] = query_voted(context.get("packages"), request.user) # Packages that request.user is being notified about. context["packages_notified"] = query_notified( - context.get("packages"), request.user) + context.get("packages"), request.user + ) # Any packages that the request user comaintains. - context["comaintained"] = packages.join( - models.PackageComaintainer - ).filter( - models.PackageComaintainer.UsersID == request.user.ID - ).order_by( - models.PackageBase.ModifiedTS.desc(), models.Package.Name.desc() - ).limit(50).all() + context["comaintained"] = ( + packages.join(models.PackageComaintainer) + .filter(models.PackageComaintainer.UsersID == request.user.ID) + .order_by(models.PackageBase.ModifiedTS.desc(), models.Package.Name.desc()) + .limit(50) + .all() + ) # Comaintained packages that request.user has voted for. context["comaintained_voted"] = query_voted( - context.get("comaintained"), request.user) + context.get("comaintained"), request.user + ) # Comaintained packages that request.user is being notified about. context["comaintained_notified"] = query_notified( - context.get("comaintained"), request.user) + context.get("comaintained"), request.user + ) return render_template(request, "index.html", context) @@ -232,16 +207,18 @@ async def archive_sha256(request: Request, archive: str): @router.get("/metrics") async def metrics(request: Request): if not os.environ.get("PROMETHEUS_MULTIPROC_DIR", None): - return Response("Prometheus metrics are not enabled.", - status_code=HTTPStatus.SERVICE_UNAVAILABLE) + return Response( + "Prometheus metrics are not enabled.", + status_code=HTTPStatus.SERVICE_UNAVAILABLE, + ) + + # update prometheus gauges for packages and users + statistics.update_prometheus_metrics() registry = CollectorRegistry() multiprocess.MultiProcessCollector(registry) data = generate_latest(registry) - headers = { - "Content-Type": CONTENT_TYPE_LATEST, - "Content-Length": str(len(data)) - } + headers = {"Content-Type": CONTENT_TYPE_LATEST, "Content-Length": str(len(data))} return Response(data, headers=headers) diff --git a/aurweb/routers/package_maintainer.py b/aurweb/routers/package_maintainer.py new file mode 100644 index 00000000..9ce38d07 --- /dev/null +++ b/aurweb/routers/package_maintainer.py @@ -0,0 +1,394 @@ +import html +import typing +from http import HTTPStatus +from typing import Any + +from fastapi import APIRouter, Form, HTTPException, Request +from fastapi.responses import RedirectResponse, Response +from sqlalchemy import and_, func, or_ + +from aurweb import aur_logging, db, l10n, models, time +from aurweb.auth import creds, requires_auth +from aurweb.exceptions import handle_form_exceptions +from aurweb.models import User +from aurweb.models.account_type import ( + PACKAGE_MAINTAINER_AND_DEV_ID, + PACKAGE_MAINTAINER_ID, +) +from aurweb.templates import make_context, make_variable_context, render_template + +router = APIRouter() +logger = aur_logging.get_logger(__name__) + +# Some PM route specific constants. +ITEMS_PER_PAGE = 10 # Paged table size. +MAX_AGENDA_LENGTH = 75 # Agenda table column length. + +ADDVOTE_SPECIFICS = { + # This dict stores a vote duration and quorum for a proposal. + # When a proposal is added, duration is added to the current + # timestamp. + # "addvote_type": (duration, quorum) + "add_pm": (7 * 24 * 60 * 60, 0.66), + "remove_pm": (7 * 24 * 60 * 60, 0.75), + "remove_inactive_pm": (5 * 24 * 60 * 60, 0.66), + "bylaws": (7 * 24 * 60 * 60, 0.75), +} + + +def populate_package_maintainer_counts(context: dict[str, Any]) -> None: + pm_query = db.query(User).filter( + or_( + User.AccountTypeID == PACKAGE_MAINTAINER_ID, + User.AccountTypeID == PACKAGE_MAINTAINER_AND_DEV_ID, + ) + ) + context["package_maintainer_count"] = pm_query.count() + + # In case any records have a None InactivityTS. + active_pm_query = pm_query.filter( + or_(User.InactivityTS.is_(None), User.InactivityTS == 0) + ) + context["active_package_maintainer_count"] = active_pm_query.count() + + +@router.get("/package-maintainer") +@requires_auth +async def package_maintainer( + request: Request, + coff: int = 0, # current offset + cby: str = "desc", # current by + poff: int = 0, # past offset + pby: str = "desc", +): # past by + """Proposal listings.""" + + if not request.user.has_credential(creds.PM_LIST_VOTES): + return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER) + + context = make_context(request, "Package Maintainer") + + current_by, past_by = cby, pby + current_off, past_off = coff, poff + + context["pp"] = pp = ITEMS_PER_PAGE + context["prev_len"] = MAX_AGENDA_LENGTH + + ts = time.utcnow() + + if current_by not in {"asc", "desc"}: + # If a malicious by was given, default to desc. + current_by = "desc" + context["current_by"] = current_by + + if past_by not in {"asc", "desc"}: + # If a malicious by was given, default to desc. + past_by = "desc" + context["past_by"] = past_by + + current_votes = ( + db.query(models.VoteInfo) + .filter(models.VoteInfo.End > ts) + .order_by(models.VoteInfo.Submitted.desc()) + ) + context["current_votes_count"] = current_votes.count() + current_votes = current_votes.limit(pp).offset(current_off) + context["current_votes"] = ( + reversed(current_votes.all()) if current_by == "asc" else current_votes.all() + ) + context["current_off"] = current_off + + past_votes = ( + db.query(models.VoteInfo) + .filter(models.VoteInfo.End <= ts) + .order_by(models.VoteInfo.Submitted.desc()) + ) + context["past_votes_count"] = past_votes.count() + past_votes = past_votes.limit(pp).offset(past_off) + context["past_votes"] = ( + reversed(past_votes.all()) if past_by == "asc" else past_votes.all() + ) + context["past_off"] = past_off + + last_vote = func.max(models.Vote.VoteID).label("LastVote") + last_votes_by_pm = ( + db.query(models.Vote) + .join(models.User) + .join(models.VoteInfo, models.VoteInfo.ID == models.Vote.VoteID) + .filter( + and_( + models.Vote.VoteID == models.VoteInfo.ID, + models.User.ID == models.Vote.UserID, + models.VoteInfo.End < ts, + or_(models.User.AccountTypeID == 2, models.User.AccountTypeID == 4), + ) + ) + .with_entities(models.Vote.UserID, last_vote, models.User.Username) + .group_by(models.Vote.UserID) + .order_by(last_vote.desc(), models.User.Username.asc()) + ) + context["last_votes_by_pm"] = last_votes_by_pm.all() + + context["current_by_next"] = "asc" if current_by == "desc" else "desc" + context["past_by_next"] = "asc" if past_by == "desc" else "desc" + + populate_package_maintainer_counts(context) + + context["q"] = { + "coff": current_off, + "cby": current_by, + "poff": past_off, + "pby": past_by, + } + + return render_template(request, "package-maintainer/index.html", context) + + +def render_proposal( + request: Request, + context: dict, + proposal: int, + voteinfo: models.VoteInfo, + voters: typing.Iterable[models.User], + vote: models.Vote, + status_code: HTTPStatus = HTTPStatus.OK, +): + """Render a single PM proposal.""" + context["proposal"] = proposal + context["voteinfo"] = voteinfo + context["voters"] = voters.all() + + total = voteinfo.total_votes() + participation = (total / voteinfo.ActiveUsers) if voteinfo.ActiveUsers else 0 + context["participation"] = participation + + accepted = (voteinfo.Yes > voteinfo.ActiveUsers / 2) or ( + participation > voteinfo.Quorum and voteinfo.Yes > voteinfo.No + ) + context["accepted"] = accepted + + can_vote = voters.filter(models.Vote.User == request.user).first() is None + context["can_vote"] = can_vote + + if not voteinfo.is_running(): + context["error"] = "Voting is closed for this proposal." + + context["vote"] = vote + context["has_voted"] = vote is not None + + return render_template( + request, "package-maintainer/show.html", context, status_code=status_code + ) + + +@router.get("/package-maintainer/{proposal}") +@requires_auth +async def package_maintainer_proposal(request: Request, proposal: int): + if not request.user.has_credential(creds.PM_LIST_VOTES): + return RedirectResponse("/package-maintainer", status_code=HTTPStatus.SEE_OTHER) + + context = await make_variable_context(request, "Package Maintainer") + proposal = int(proposal) + + voteinfo = db.query(models.VoteInfo).filter(models.VoteInfo.ID == proposal).first() + if not voteinfo: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND) + + voters = ( + db.query(models.User) + .join(models.Vote) + .filter(models.Vote.VoteID == voteinfo.ID) + ) + vote = ( + db.query(models.Vote) + .filter( + and_( + models.Vote.UserID == request.user.ID, + models.Vote.VoteID == voteinfo.ID, + ) + ) + .first() + ) + if not request.user.has_credential(creds.PM_VOTE): + context["error"] = "Only Package Maintainers are allowed to vote." + if voteinfo.User == request.user.Username: + context["error"] = "You cannot vote in an proposal about you." + elif vote is not None: + context["error"] = "You've already voted for this proposal." + + context["vote"] = vote + return render_proposal(request, context, proposal, voteinfo, voters, vote) + + +@db.async_retry_deadlock +@router.post("/package-maintainer/{proposal}") +@handle_form_exceptions +@requires_auth +async def package_maintainer_proposal_post( + request: Request, proposal: int, decision: str = Form(...) +): + if not request.user.has_credential(creds.PM_LIST_VOTES): + return RedirectResponse("/package-maintainer", status_code=HTTPStatus.SEE_OTHER) + + context = await make_variable_context(request, "Package Maintainer") + proposal = int(proposal) # Make sure it's an int. + + voteinfo = db.query(models.VoteInfo).filter(models.VoteInfo.ID == proposal).first() + if not voteinfo: + raise HTTPException(status_code=HTTPStatus.NOT_FOUND) + + voters = ( + db.query(models.User) + .join(models.Vote) + .filter(models.Vote.VoteID == voteinfo.ID) + ) + vote = ( + db.query(models.Vote) + .filter( + and_( + models.Vote.UserID == request.user.ID, + models.Vote.VoteID == voteinfo.ID, + ) + ) + .first() + ) + + status_code = HTTPStatus.OK + if not request.user.has_credential(creds.PM_VOTE): + context["error"] = "Only Package Maintainers are allowed to vote." + status_code = HTTPStatus.UNAUTHORIZED + elif voteinfo.User == request.user.Username: + context["error"] = "You cannot vote in an proposal about you." + status_code = HTTPStatus.BAD_REQUEST + elif vote is not None: + context["error"] = "You've already voted for this proposal." + status_code = HTTPStatus.BAD_REQUEST + + if status_code != HTTPStatus.OK: + return render_proposal( + request, context, proposal, voteinfo, voters, vote, status_code=status_code + ) + + with db.begin(): + if decision in {"Yes", "No", "Abstain"}: + # Increment whichever decision was given to us. + setattr(voteinfo, decision, getattr(voteinfo, decision) + 1) + else: + return Response( + "Invalid 'decision' value.", status_code=HTTPStatus.BAD_REQUEST + ) + + vote = db.create(models.Vote, User=request.user, VoteInfo=voteinfo) + + context["error"] = "You've already voted for this proposal." + return render_proposal(request, context, proposal, voteinfo, voters, vote) + + +@router.get("/addvote") +@requires_auth +async def package_maintainer_addvote( + request: Request, user: str = str(), type: str = "add_pm", agenda: str = str() +): + if not request.user.has_credential(creds.PM_ADD_VOTE): + return RedirectResponse("/package-maintainer", status_code=HTTPStatus.SEE_OTHER) + + context = await make_variable_context(request, "Add Proposal") + + if type not in ADDVOTE_SPECIFICS: + context["error"] = "Invalid type." + type = "add_pm" # Default it. + + context["user"] = user + context["type"] = type + context["agenda"] = agenda + + return render_template(request, "addvote.html", context) + + +@db.async_retry_deadlock +@router.post("/addvote") +@handle_form_exceptions +@requires_auth +async def package_maintainer_addvote_post( + request: Request, + user: str = Form(default=str()), + type: str = Form(default=str()), + agenda: str = Form(default=str()), +): + if not request.user.has_credential(creds.PM_ADD_VOTE): + return RedirectResponse("/package-maintainer", status_code=HTTPStatus.SEE_OTHER) + + # Build a context. + context = await make_variable_context(request, "Add Proposal") + + context["type"] = type + context["user"] = user + context["agenda"] = agenda + + def render_addvote(context, status_code): + """Simplify render_template a bit for this test.""" + return render_template(request, "addvote.html", context, status_code) + + # Alright, get some database records, if we can. + if type != "bylaws": + user_record = db.query(models.User).filter(models.User.Username == user).first() + if user_record is None: + context["error"] = "Username does not exist." + return render_addvote(context, HTTPStatus.NOT_FOUND) + + utcnow = time.utcnow() + voteinfo = ( + db.query(models.VoteInfo) + .filter(and_(models.VoteInfo.User == user, models.VoteInfo.End > utcnow)) + .count() + ) + if voteinfo: + _ = l10n.get_translator_for_request(request) + context["error"] = _("%s already has proposal running for them.") % ( + html.escape(user), + ) + return render_addvote(context, HTTPStatus.BAD_REQUEST) + + if type not in ADDVOTE_SPECIFICS: + context["error"] = "Invalid type." + context["type"] = type = "add_pm" # Default for rendering. + return render_addvote(context, HTTPStatus.BAD_REQUEST) + + if not agenda: + context["error"] = "Proposal cannot be empty." + return render_addvote(context, HTTPStatus.BAD_REQUEST) + + # Gather some mapped constants and the current timestamp. + duration, quorum = ADDVOTE_SPECIFICS.get(type) + timestamp = time.utcnow() + + # Active PM types we filter for. + types = {PACKAGE_MAINTAINER_ID, PACKAGE_MAINTAINER_AND_DEV_ID} + + # Create a new VoteInfo (proposal)! + with db.begin(): + active_pms = ( + db.query(User) + .filter( + and_( + User.Suspended == 0, + User.InactivityTS.isnot(None), + User.AccountTypeID.in_(types), + ) + ) + .count() + ) + voteinfo = db.create( + models.VoteInfo, + User=user, + Agenda=html.escape(agenda), + Submitted=timestamp, + End=(timestamp + duration), + Quorum=quorum, + ActiveUsers=active_pms, + Submitter=request.user, + ) + + # Redirect to the new proposal. + endpoint = f"/package-maintainer/{voteinfo.ID}" + return RedirectResponse(endpoint, status_code=HTTPStatus.SEE_OTHER) diff --git a/aurweb/routers/packages.py b/aurweb/routers/packages.py index bc12455d..13f30494 100644 --- a/aurweb/routers/packages.py +++ b/aurweb/routers/packages.py @@ -1,39 +1,41 @@ from collections import defaultdict from http import HTTPStatus -from typing import Any, Dict, List +from typing import Any -from fastapi import APIRouter, Form, Request, Response +from fastapi import APIRouter, Form, Query, Request, Response import aurweb.filters # noqa: F401 - -from aurweb import config, db, defaults, logging, models, util +from aurweb import aur_logging, config, db, defaults, models, util from aurweb.auth import creds, requires_auth +from aurweb.cache import db_count_cache, db_query_cache from aurweb.exceptions import InvariantError, handle_form_exceptions from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID from aurweb.packages import util as pkgutil from aurweb.packages.search import PackageSearch from aurweb.packages.util import get_pkg_or_base -from aurweb.pkgbase import actions as pkgbase_actions -from aurweb.pkgbase import util as pkgbaseutil +from aurweb.pkgbase import actions as pkgbase_actions, util as pkgbaseutil from aurweb.templates import make_context, make_variable_context, render_template +from aurweb.util import hash_query -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) router = APIRouter() -async def packages_get(request: Request, context: Dict[str, Any], - status_code: HTTPStatus = HTTPStatus.OK): +async def packages_get( + request: Request, context: dict[str, Any], status_code: HTTPStatus = HTTPStatus.OK +): # Query parameters used in this request. context["q"] = dict(request.query_params) # Per page and offset. offset, per_page = util.sanitize_params( request.query_params.get("O", defaults.O), - request.query_params.get("PP", defaults.PP)) + request.query_params.get("PP", defaults.PP), + ) context["O"] = offset # Limit PP to options.max_search_results - max_search_results = aurweb.config.getint("options", "max_search_results") + max_search_results = config.getint("options", "max_search_results") context["PP"] = per_page = min(per_page, max_search_results) # Query search by. @@ -82,13 +84,14 @@ async def packages_get(request: Request, context: Dict[str, Any], if submit == "Orphans": # If the user clicked the "Orphans" button, we only want # orphaned packages. - search.query = search.query.filter( - models.PackageBase.MaintainerUID.is_(None)) + search.query = search.query.filter(models.PackageBase.MaintainerUID.is_(None)) # Collect search result count here; we've applied our keywords. # Including more query operations below, like ordering, will # increase the amount of time required to collect a count. - num_packages = search.count() + # we use redis for caching the results of the query + cache_expire = config.getint("cache", "expiry_time_search", 600) + num_packages = db_count_cache(hash_query(search.query), search.query, cache_expire) # Apply user-specified sort column and ordering. search.sort_by(sort_by, sort_order) @@ -103,17 +106,24 @@ async def packages_get(request: Request, context: Dict[str, Any], models.PackageBase.Popularity, models.PackageBase.NumVotes, models.PackageBase.OutOfDateTS, + models.PackageBase.ModifiedTS, models.User.Username.label("Maintainer"), models.PackageVote.PackageBaseID.label("Voted"), - models.PackageNotification.PackageBaseID.label("Notify") - ).group_by(models.Package.Name) + models.PackageNotification.PackageBaseID.label("Notify"), + ) + + # paging + results = results.limit(per_page).offset(offset) + + # we use redis for caching the results of the query + packages = db_query_cache(hash_query(results), results, cache_expire) - packages = results.limit(per_page).offset(offset) context["packages"] = packages context["packages_count"] = num_packages - return render_template(request, "packages/index.html", context, - status_code=status_code) + return render_template( + request, "packages/index.html", context, status_code=status_code + ) @router.get("/packages") @@ -123,7 +133,25 @@ async def packages(request: Request) -> Response: @router.get("/packages/{name}") -async def package(request: Request, name: str) -> Response: +async def package( + request: Request, + name: str, + all_deps: bool = Query(default=False), + all_reqs: bool = Query(default=False), +) -> Response: + """ + Get a package by name. + + By default, we limit the number of depends and requires results + to 20. To bypass this and load all of them, which should be triggered + via a "Show more" link near the limited listing. + + :param name: Package.Name + :param all_deps: Boolean indicating whether we should load all depends + :param all_reqs: Boolean indicating whether we should load all requires + :return: FastAPI Response + """ + # Get the Package. pkg = get_pkg_or_base(name, models.Package) pkgbase = pkg.PackageBase @@ -140,25 +168,51 @@ async def package(request: Request, name: str) -> Response: # Add our base information. context = pkgbaseutil.make_context(request, pkgbase) + context["q"] = dict(request.query_params) + + context.update({"all_deps": all_deps, "all_reqs": all_reqs}) + context["package"] = pkg # Package sources. context["sources"] = pkg.package_sources.order_by( - models.PackageSource.Source.asc()).all() + models.PackageSource.Source.asc() + ).all() + + # Listing metadata. + context["max_listing"] = max_listing = 20 # Package dependencies. - max_depends = config.getint("options", "max_depends") - context["dependencies"] = pkg.package_dependencies.order_by( - models.PackageDependency.DepTypeID.asc(), - models.PackageDependency.DepName.asc() - ).limit(max_depends).all() + deps = pkg.package_dependencies.order_by( + models.PackageDependency.DepTypeID.asc(), models.PackageDependency.DepName.asc() + ) + context["depends_count"] = deps.count() + if not all_deps: + deps = deps.limit(max_listing) + context["dependencies"] = deps.all() + # Existing dependencies to avoid multiple lookups + context["dependencies_names_from_aur"] = [ + item.Name + for item in db.query(models.Package) + .filter( + models.Package.Name.in_( + pkg.package_dependencies.with_entities(models.PackageDependency.DepName) + ) + ) + .all() + ] # Package requirements (other packages depend on this one). - context["required_by"] = pkgutil.pkg_required( - pkg.Name, [p.RelName for p in rels_data.get("p", [])], max_depends) + reqs = pkgutil.pkg_required(pkg.Name, [p.RelName for p in rels_data.get("p", [])]) + context["reqs_count"] = reqs.count() + if not all_reqs: + reqs = reqs.limit(max_listing) + context["required_by"] = reqs.all() context["licenses"] = pkg.package_licenses + context["groups"] = pkg.package_groups + conflicts = pkg.package_relations.filter( models.PackageRelation.RelTypeID == CONFLICTS_ID ).order_by(models.PackageRelation.RelName.asc()) @@ -177,46 +231,42 @@ async def package(request: Request, name: str) -> Response: return render_template(request, "packages/show.html", context) -async def packages_unflag(request: Request, package_ids: List[int] = [], - **kwargs): +async def packages_unflag(request: Request, package_ids: list[int] = [], **kwargs): if not package_ids: - return (False, ["You did not select any packages to unflag."]) + return False, ["You did not select any packages to unflag."] # Holds the set of package bases we're looking to unflag. # Constructed below via looping through the packages query. bases = set() package_ids = set(package_ids) # Convert this to a set for O(1). - packages = db.query(models.Package).filter( - models.Package.ID.in_(package_ids)).all() + packages = db.query(models.Package).filter(models.Package.ID.in_(package_ids)).all() for pkg in packages: has_cred = request.user.has_credential( - creds.PKGBASE_UNFLAG, approved=[pkg.PackageBase.Flagger]) + creds.PKGBASE_UNFLAG, approved=[pkg.PackageBase.Flagger] + ) if not has_cred: - return (False, ["You did not select any packages to unflag."]) + return False, ["You did not select any packages to unflag."] if pkg.PackageBase not in bases: bases.update({pkg.PackageBase}) for pkgbase in bases: pkgbase_actions.pkgbase_unflag_instance(request, pkgbase) - return (True, ["The selected packages have been unflagged."]) + return True, ["The selected packages have been unflagged."] -async def packages_notify(request: Request, package_ids: List[int] = [], - **kwargs): +async def packages_notify(request: Request, package_ids: list[int] = [], **kwargs): # In cases where we encounter errors with the request, we'll # use this error tuple as a return value. # TODO: This error does not yet have a translation. - error_tuple = (False, - ["You did not select any packages to be notified about."]) + error_tuple = (False, ["You did not select any packages to be notified about."]) if not package_ids: return error_tuple bases = set() package_ids = set(package_ids) - packages = db.query(models.Package).filter( - models.Package.ID.in_(package_ids)).all() + packages = db.query(models.Package).filter(models.Package.ID.in_(package_ids)).all() for pkg in packages: if pkg.PackageBase not in bases: @@ -224,9 +274,11 @@ async def packages_notify(request: Request, package_ids: List[int] = [], # Perform some checks on what the user selected for notify. for pkgbase in bases: - notif = db.query(pkgbase.notifications.filter( - models.PackageNotification.UserID == request.user.ID - ).exists()).scalar() + notif = db.query( + pkgbase.notifications.filter( + models.PackageNotification.UserID == request.user.ID + ).exists() + ).scalar() has_cred = request.user.has_credential(creds.PKGBASE_NOTIFY) # If the request user either does not have credentials @@ -239,26 +291,23 @@ async def packages_notify(request: Request, package_ids: List[int] = [], pkgbase_actions.pkgbase_notify_instance(request, pkgbase) # TODO: This message does not yet have a translation. - return (True, ["The selected packages' notifications have been enabled."]) + return True, ["The selected packages' notifications have been enabled."] -async def packages_unnotify(request: Request, package_ids: List[int] = [], - **kwargs): +async def packages_unnotify(request: Request, package_ids: list[int] = [], **kwargs): if not package_ids: # TODO: This error does not yet have a translation. - return (False, - ["You did not select any packages for notification removal."]) + return False, ["You did not select any packages for notification removal."] # TODO: This error does not yet have a translation. error_tuple = ( False, - ["A package you selected does not have notifications enabled."] + ["A package you selected does not have notifications enabled."], ) bases = set() package_ids = set(package_ids) - packages = db.query(models.Package).filter( - models.Package.ID.in_(package_ids)).all() + packages = db.query(models.Package).filter(models.Package.ID.in_(package_ids)).all() for pkg in packages: if pkg.PackageBase not in bases: @@ -266,9 +315,11 @@ async def packages_unnotify(request: Request, package_ids: List[int] = [], # Perform some checks on what the user selected for notify. for pkgbase in bases: - notif = db.query(pkgbase.notifications.filter( - models.PackageNotification.UserID == request.user.ID - ).exists()).scalar() + notif = db.query( + pkgbase.notifications.filter( + models.PackageNotification.UserID == request.user.ID + ).exists() + ).scalar() if not notif: return error_tuple @@ -276,22 +327,27 @@ async def packages_unnotify(request: Request, package_ids: List[int] = [], pkgbase_actions.pkgbase_unnotify_instance(request, pkgbase) # TODO: This message does not yet have a translation. - return (True, ["The selected packages' notifications have been removed."]) + return True, ["The selected packages' notifications have been removed."] -async def packages_adopt(request: Request, package_ids: List[int] = [], - confirm: bool = False, **kwargs): +async def packages_adopt( + request: Request, package_ids: list[int] = [], confirm: bool = False, **kwargs +): if not package_ids: - return (False, ["You did not select any packages to adopt."]) + return False, ["You did not select any packages to adopt."] if not confirm: - return (False, ["The selected packages have not been adopted, " - "check the confirmation checkbox."]) + return ( + False, + [ + "The selected packages have not been adopted, " + "check the confirmation checkbox." + ], + ) bases = set() package_ids = set(package_ids) - packages = db.query(models.Package).filter( - models.Package.ID.in_(package_ids)).all() + packages = db.query(models.Package).filter(models.Package.ID.in_(package_ids)).all() for pkg in packages: if pkg.PackageBase not in bases: @@ -302,18 +358,19 @@ async def packages_adopt(request: Request, package_ids: List[int] = [], has_cred = request.user.has_credential(creds.PKGBASE_ADOPT) if not (has_cred or not pkgbase.Maintainer): # TODO: This error needs to be translated. - return (False, ["You are not allowed to adopt one of the " - "packages you selected."]) + return ( + False, + ["You are not allowed to adopt one of the " "packages you selected."], + ) # Now, really adopt the bases. for pkgbase in bases: pkgbase_actions.pkgbase_adopt_instance(request, pkgbase) - return (True, ["The selected packages have been adopted."]) + return True, ["The selected packages have been adopted."] -def disown_all(request: Request, pkgbases: List[models.PackageBase]) \ - -> List[str]: +def disown_all(request: Request, pkgbases: list[models.PackageBase]) -> list[str]: errors = [] for pkgbase in pkgbases: try: @@ -323,19 +380,24 @@ def disown_all(request: Request, pkgbases: List[models.PackageBase]) \ return errors -async def packages_disown(request: Request, package_ids: List[int] = [], - confirm: bool = False, **kwargs): +async def packages_disown( + request: Request, package_ids: list[int] = [], confirm: bool = False, **kwargs +): if not package_ids: - return (False, ["You did not select any packages to disown."]) + return False, ["You did not select any packages to disown."] if not confirm: - return (False, ["The selected packages have not been disowned, " - "check the confirmation checkbox."]) + return ( + False, + [ + "The selected packages have not been disowned, " + "check the confirmation checkbox." + ], + ) bases = set() package_ids = set(package_ids) - packages = db.query(models.Package).filter( - models.Package.ID.in_(package_ids)).all() + packages = db.query(models.Package).filter(models.Package.ID.in_(package_ids)).all() for pkg in packages: if pkg.PackageBase not in bases: @@ -343,43 +405,54 @@ async def packages_disown(request: Request, package_ids: List[int] = [], # Check that the user has credentials for every package they selected. for pkgbase in bases: - has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, - approved=[pkgbase.Maintainer]) + has_cred = request.user.has_credential( + creds.PKGBASE_DISOWN, approved=[pkgbase.Maintainer] + ) if not has_cred: # TODO: This error needs to be translated. - return (False, ["You are not allowed to disown one " - "of the packages you selected."]) + return ( + False, + ["You are not allowed to disown one " "of the packages you selected."], + ) # Now, disown all the bases if we can. if errors := disown_all(request, bases): - return (False, errors) + return False, errors - return (True, ["The selected packages have been disowned."]) + return True, ["The selected packages have been disowned."] -async def packages_delete(request: Request, package_ids: List[int] = [], - confirm: bool = False, merge_into: str = str(), - **kwargs): +async def packages_delete( + request: Request, + package_ids: list[int] = [], + confirm: bool = False, + merge_into: str = str(), + **kwargs, +): if not package_ids: - return (False, ["You did not select any packages to delete."]) + return False, ["You did not select any packages to delete."] if not confirm: - return (False, ["The selected packages have not been deleted, " - "check the confirmation checkbox."]) + return ( + False, + [ + "The selected packages have not been deleted, " + "check the confirmation checkbox." + ], + ) if not request.user.has_credential(creds.PKGBASE_DELETE): - return (False, ["You do not have permission to delete packages."]) + return False, ["You do not have permission to delete packages."] # set-ify package_ids and query the database for related records. package_ids = set(package_ids) - packages = db.query(models.Package).filter( - models.Package.ID.in_(package_ids)).all() + packages = db.query(models.Package).filter(models.Package.ID.in_(package_ids)).all() if len(packages) != len(package_ids): # Let the user know there was an issue with their input: they have # provided at least one package_id which does not exist in the DB. # TODO: This error has not yet been translated. - return (False, ["One of the packages you selected does not exist."]) + return False, ["One of the packages you selected does not exist."] # Make a set out of all package bases related to `packages`. bases = {pkg.PackageBase for pkg in packages} @@ -389,15 +462,18 @@ async def packages_delete(request: Request, package_ids: List[int] = [], notifs += pkgbase_actions.pkgbase_delete_instance(request, pkgbase) # Log out the fact that this happened for accountability. - logger.info(f"Privileged user '{request.user.Username}' deleted the " - f"following package bases: {str(deleted_bases)}.") + logger.info( + f"Privileged user '{request.user.Username}' deleted the " + f"following package bases: {str(deleted_bases)}." + ) util.apply_all(notifs, lambda n: n.send()) - return (True, ["The selected packages have been deleted."]) + return True, ["The selected packages have been deleted."] + # A mapping of action string -> callback functions used within the # `packages_post` route below. We expect any action callback to -# return a tuple in the format: (succeeded: bool, message: List[str]). +# return a tuple in the format: (succeeded: bool, message: list[str]). PACKAGE_ACTIONS = { "unflag": packages_unflag, "notify": packages_notify, @@ -411,11 +487,12 @@ PACKAGE_ACTIONS = { @router.post("/packages") @handle_form_exceptions @requires_auth -async def packages_post(request: Request, - IDs: List[int] = Form(default=[]), - action: str = Form(default=str()), - confirm: bool = Form(default=False)): - +async def packages_post( + request: Request, + IDs: list[int] = Form(default=[]), + action: str = Form(default=str()), + confirm: bool = Form(default=False), +): # If an invalid action is specified, just render GET /packages # with an BAD_REQUEST status_code. if action not in PACKAGE_ACTIONS: diff --git a/aurweb/routers/pkgbase.py b/aurweb/routers/pkgbase.py index 2cef5436..213348cd 100644 --- a/aurweb/routers/pkgbase.py +++ b/aurweb/routers/pkgbase.py @@ -4,7 +4,7 @@ from fastapi import APIRouter, Form, HTTPException, Query, Request, Response from fastapi.responses import JSONResponse, RedirectResponse from sqlalchemy import and_ -from aurweb import config, db, l10n, logging, templates, time, util +from aurweb import aur_logging, config, db, l10n, templates, time, util from aurweb.auth import creds, requires_auth from aurweb.exceptions import InvariantError, ValidationError, handle_form_exceptions from aurweb.models import PackageBase @@ -16,14 +16,12 @@ from aurweb.models.package_vote import PackageVote from aurweb.models.request_type import DELETION_ID, MERGE_ID, ORPHAN_ID from aurweb.packages.requests import update_closure_comment from aurweb.packages.util import get_pkg_or_base, get_pkgbase_comment -from aurweb.pkgbase import actions -from aurweb.pkgbase import util as pkgbaseutil -from aurweb.pkgbase import validate +from aurweb.pkgbase import actions, util as pkgbaseutil, validate from aurweb.scripts import notify, popupdate from aurweb.scripts.rendercomment import update_comment_render_fastapi from aurweb.templates import make_variable_context, render_template -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) router = APIRouter() @@ -44,8 +42,9 @@ async def pkgbase(request: Request, name: str) -> Response: packages = pkgbase.packages.all() pkg = packages[0] if len(packages) == 1 and pkg.Name == pkgbase.Name: - return RedirectResponse(f"/packages/{pkg.Name}", - status_code=int(HTTPStatus.SEE_OTHER)) + return RedirectResponse( + f"/packages/{pkg.Name}", status_code=int(HTTPStatus.SEE_OTHER) + ) # Add our base information. context = pkgbaseutil.make_context(request, pkgbase) @@ -69,8 +68,7 @@ async def pkgbase_voters(request: Request, name: str) -> Response: pkgbase = get_pkg_or_base(name, PackageBase) if not request.user.has_credential(creds.PKGBASE_LIST_VOTERS): - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) context = templates.make_context(request, "Voters") context["pkgbase"] = pkgbase @@ -82,35 +80,42 @@ async def pkgbase_flag_comment(request: Request, name: str): pkgbase = get_pkg_or_base(name, PackageBase) if pkgbase.OutOfDateTS is None: - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) context = templates.make_context(request, "Flag Comment") context["pkgbase"] = pkgbase return render_template(request, "pkgbase/flag-comment.html", context) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/keywords") @handle_form_exceptions -async def pkgbase_keywords(request: Request, name: str, - keywords: str = Form(default=str())): +async def pkgbase_keywords( + request: Request, name: str, keywords: str = Form(default=str()) +): pkgbase = get_pkg_or_base(name, PackageBase) + approved = [pkgbase.Maintainer] + [c.User for c in pkgbase.comaintainers] + has_cred = creds.has_credential( + request.user, creds.PKGBASE_SET_KEYWORDS, approved=approved + ) + if not has_cred: + return Response(status_code=HTTPStatus.UNAUTHORIZED) + # Lowercase all keywords. Our database table is case insensitive, # and providing CI duplicates of keywords is erroneous. - keywords = set(k.lower() for k in keywords.split(" ")) + keywords = set(k.lower() for k in keywords.split()) # Delete all keywords which are not supplied by the user. with db.begin(): - other_keywords = pkgbase.keywords.filter( - ~PackageKeyword.Keyword.in_(keywords)) - other_keyword_strings = set( - kwd.Keyword.lower() for kwd in other_keywords) + other_keywords = pkgbase.keywords.filter(~PackageKeyword.Keyword.in_(keywords)) + other_keyword_strings = set(kwd.Keyword.lower() for kwd in other_keywords) existing_keywords = set( - kwd.Keyword.lower() for kwd in - pkgbase.keywords.filter( - ~PackageKeyword.Keyword.in_(other_keyword_strings)) + kwd.Keyword.lower() + for kwd in pkgbase.keywords.filter( + ~PackageKeyword.Keyword.in_(other_keyword_strings) + ) ) db.delete_all(other_keywords) @@ -118,8 +123,7 @@ async def pkgbase_keywords(request: Request, name: str, for keyword in new_keywords: db.create(PackageKeyword, PackageBase=pkgbase, Keyword=keyword) - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) @router.get("/pkgbase/{name}/flag") @@ -129,28 +133,33 @@ async def pkgbase_flag_get(request: Request, name: str): has_cred = request.user.has_credential(creds.PKGBASE_FLAG) if not has_cred or pkgbase.OutOfDateTS is not None: - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) context = templates.make_context(request, "Flag Package Out-Of-Date") context["pkgbase"] = pkgbase return render_template(request, "pkgbase/flag.html", context) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/flag") @handle_form_exceptions @requires_auth -async def pkgbase_flag_post(request: Request, name: str, - comments: str = Form(default=str())): +async def pkgbase_flag_post( + request: Request, name: str, comments: str = Form(default=str()) +): pkgbase = get_pkg_or_base(name, PackageBase) if not comments: context = templates.make_context(request, "Flag Package Out-Of-Date") context["pkgbase"] = pkgbase - context["errors"] = ["The selected packages have not been flagged, " - "please enter a comment."] - return render_template(request, "pkgbase/flag.html", context, - status_code=HTTPStatus.BAD_REQUEST) + context["errors"] = [ + "The selected packages have not been flagged, " "please enter a comment." + ] + return render_template( + request, "pkgbase/flag.html", context, status_code=HTTPStatus.BAD_REQUEST + ) + + validate.comment_raise_http_ex(comments) has_cred = request.user.has_credential(creds.PKGBASE_FLAG) if has_cred and not pkgbase.OutOfDateTS: @@ -162,50 +171,56 @@ async def pkgbase_flag_post(request: Request, name: str, notify.FlagNotification(request.user.ID, pkgbase.ID).send() - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/comments") @handle_form_exceptions @requires_auth async def pkgbase_comments_post( - request: Request, name: str, - comment: str = Form(default=str()), - enable_notifications: bool = Form(default=False)): - """ Add a new comment via POST request. """ + request: Request, + name: str, + comment: str = Form(default=str()), + enable_notifications: bool = Form(default=False), +): + """Add a new comment via POST request.""" pkgbase = get_pkg_or_base(name, PackageBase) - if not comment: - raise HTTPException(status_code=HTTPStatus.BAD_REQUEST) + validate.comment_raise_http_ex(comment) # If the provided comment is different than the record's version, # update the db record. now = time.utcnow() with db.begin(): - comment = db.create(PackageComment, User=request.user, - PackageBase=pkgbase, - Comments=comment, RenderedComment=str(), - CommentTS=now) + comment = db.create( + PackageComment, + User=request.user, + PackageBase=pkgbase, + Comments=comment, + RenderedComment=str(), + CommentTS=now, + ) if enable_notifications and not request.user.notified(pkgbase): - db.create(PackageNotification, - User=request.user, - PackageBase=pkgbase) + db.create(PackageNotification, User=request.user, PackageBase=pkgbase) update_comment_render_fastapi(comment) notif = notify.CommentNotification(request.user.ID, pkgbase.ID, comment.ID) notif.send() # Redirect to the pkgbase page. - return RedirectResponse(f"/pkgbase/{pkgbase.Name}#comment-{comment.ID}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse( + f"/pkgbase/{pkgbase.Name}#comment-{comment.ID}", + status_code=HTTPStatus.SEE_OTHER, + ) @router.get("/pkgbase/{name}/comments/{id}/form") @requires_auth -async def pkgbase_comment_form(request: Request, name: str, id: int, - next: str = Query(default=None)): +async def pkgbase_comment_form( + request: Request, name: str, id: int, next: str = Query(default=None) +): """ Produce a comment form for comment {id}. @@ -238,14 +253,16 @@ async def pkgbase_comment_form(request: Request, name: str, id: int, context["next"] = next form = templates.render_raw_template( - request, "partials/packages/comment_form.html", context) + request, "partials/packages/comment_form.html", context + ) return JSONResponse({"form": form}) @router.get("/pkgbase/{name}/comments/{id}/edit") @requires_auth -async def pkgbase_comment_edit(request: Request, name: str, id: int, - next: str = Form(default=None)): +async def pkgbase_comment_edit( + request: Request, name: str, id: int, next: str = Form(default=None) +): """ Render the non-javascript edit form. @@ -266,20 +283,32 @@ async def pkgbase_comment_edit(request: Request, name: str, id: int, return render_template(request, "pkgbase/comments/edit.html", context) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/comments/{id}") @handle_form_exceptions @requires_auth async def pkgbase_comment_post( - request: Request, name: str, id: int, - comment: str = Form(default=str()), - enable_notifications: bool = Form(default=False), - next: str = Form(default=None)): - """ Edit an existing comment. """ + request: Request, + name: str, + id: int, + comment: str = Form(default=str()), + enable_notifications: bool = Form(default=False), + next: str = Form(default=None), + cancel: bool = Form(default=False), +): + """Edit an existing comment.""" + if cancel: + return RedirectResponse( + f"/pkgbase/{name}#comment-{id}", status_code=HTTPStatus.SEE_OTHER + ) + pkgbase = get_pkg_or_base(name, PackageBase) db_comment = get_pkgbase_comment(pkgbase, id) - if not comment: - raise HTTPException(status_code=HTTPStatus.BAD_REQUEST) + validate.comment_raise_http_ex(comment) + + if request.user.ID != db_comment.UsersID: + raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED) # If the provided comment is different than the record's version, # update the db record. @@ -290,28 +319,32 @@ async def pkgbase_comment_post( db_comment.Editor = request.user db_comment.EditedTS = now + if enable_notifications: + with db.begin(): db_notif = request.user.notifications.filter( PackageNotification.PackageBaseID == pkgbase.ID ).first() - if enable_notifications and not db_notif: - db.create(PackageNotification, - User=request.user, - PackageBase=pkgbase) + if not db_notif: + db.create(PackageNotification, User=request.user, PackageBase=pkgbase) + update_comment_render_fastapi(db_comment) if not next: next = f"/pkgbase/{pkgbase.Name}" # Redirect to the pkgbase page anchored to the updated comment. - return RedirectResponse(f"{next}#comment-{db_comment.ID}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse( + f"{next}#comment-{db_comment.ID}", status_code=HTTPStatus.SEE_OTHER + ) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/comments/{id}/pin") @handle_form_exceptions @requires_auth -async def pkgbase_comment_pin(request: Request, name: str, id: int, - next: str = Form(default=None)): +async def pkgbase_comment_pin( + request: Request, name: str, id: int, next: str = Form(default=None) +): """ Pin a comment. @@ -324,13 +357,15 @@ async def pkgbase_comment_pin(request: Request, name: str, id: int, pkgbase = get_pkg_or_base(name, PackageBase) comment = get_pkgbase_comment(pkgbase, id) - has_cred = request.user.has_credential(creds.COMMENT_PIN, - approved=comment.maintainers()) + has_cred = request.user.has_credential( + creds.COMMENT_PIN, approved=comment.maintainers() + ) if not has_cred: _ = l10n.get_translator_for_request(request) raise HTTPException( status_code=HTTPStatus.UNAUTHORIZED, - detail=_("You are not allowed to pin this comment.")) + detail=_("You are not allowed to pin this comment."), + ) now = time.utcnow() with db.begin(): @@ -342,11 +377,13 @@ async def pkgbase_comment_pin(request: Request, name: str, id: int, return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/comments/{id}/unpin") @handle_form_exceptions @requires_auth -async def pkgbase_comment_unpin(request: Request, name: str, id: int, - next: str = Form(default=None)): +async def pkgbase_comment_unpin( + request: Request, name: str, id: int, next: str = Form(default=None) +): """ Unpin a comment. @@ -359,13 +396,15 @@ async def pkgbase_comment_unpin(request: Request, name: str, id: int, pkgbase = get_pkg_or_base(name, PackageBase) comment = get_pkgbase_comment(pkgbase, id) - has_cred = request.user.has_credential(creds.COMMENT_PIN, - approved=comment.maintainers()) + has_cred = request.user.has_credential( + creds.COMMENT_PIN, approved=comment.maintainers() + ) if not has_cred: _ = l10n.get_translator_for_request(request) raise HTTPException( status_code=HTTPStatus.UNAUTHORIZED, - detail=_("You are not allowed to unpin this comment.")) + detail=_("You are not allowed to unpin this comment."), + ) with db.begin(): comment.PinnedTS = 0 @@ -376,11 +415,13 @@ async def pkgbase_comment_unpin(request: Request, name: str, id: int, return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/comments/{id}/delete") @handle_form_exceptions @requires_auth -async def pkgbase_comment_delete(request: Request, name: str, id: int, - next: str = Form(default=None)): +async def pkgbase_comment_delete( + request: Request, name: str, id: int, next: str = Form(default=None) +): """ Delete a comment. @@ -397,13 +438,13 @@ async def pkgbase_comment_delete(request: Request, name: str, id: int, pkgbase = get_pkg_or_base(name, PackageBase) comment = get_pkgbase_comment(pkgbase, id) - authorized = request.user.has_credential(creds.COMMENT_DELETE, - [comment.User]) + authorized = request.user.has_credential(creds.COMMENT_DELETE, [comment.User]) if not authorized: _ = l10n.get_translator_for_request(request) raise HTTPException( status_code=HTTPStatus.UNAUTHORIZED, - detail=_("You are not allowed to delete this comment.")) + detail=_("You are not allowed to delete this comment."), + ) now = time.utcnow() with db.begin(): @@ -416,11 +457,13 @@ async def pkgbase_comment_delete(request: Request, name: str, id: int, return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/comments/{id}/undelete") @handle_form_exceptions @requires_auth -async def pkgbase_comment_undelete(request: Request, name: str, id: int, - next: str = Form(default=None)): +async def pkgbase_comment_undelete( + request: Request, name: str, id: int, next: str = Form(default=None) +): """ Undelete a comment. @@ -437,13 +480,15 @@ async def pkgbase_comment_undelete(request: Request, name: str, id: int, pkgbase = get_pkg_or_base(name, PackageBase) comment = get_pkgbase_comment(pkgbase, id) - has_cred = request.user.has_credential(creds.COMMENT_UNDELETE, - approved=[comment.User]) + has_cred = request.user.has_credential( + creds.COMMENT_UNDELETE, approved=[comment.User] + ) if not has_cred: _ = l10n.get_translator_for_request(request) raise HTTPException( status_code=HTTPStatus.UNAUTHORIZED, - detail=_("You are not allowed to undelete this comment.")) + detail=_("You are not allowed to undelete this comment."), + ) with db.begin(): comment.Deleter = None @@ -455,40 +500,34 @@ async def pkgbase_comment_undelete(request: Request, name: str, id: int, return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/vote") @handle_form_exceptions @requires_auth async def pkgbase_vote(request: Request, name: str): pkgbase = get_pkg_or_base(name, PackageBase) - vote = pkgbase.package_votes.filter( - PackageVote.UsersID == request.user.ID - ).first() + vote = pkgbase.package_votes.filter(PackageVote.UsersID == request.user.ID).first() has_cred = request.user.has_credential(creds.PKGBASE_VOTE) if has_cred and not vote: now = time.utcnow() with db.begin(): - db.create(PackageVote, - User=request.user, - PackageBase=pkgbase, - VoteTS=now) + db.create(PackageVote, User=request.user, PackageBase=pkgbase, VoteTS=now) # Update NumVotes/Popularity. popupdate.run_single(pkgbase) - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/unvote") @handle_form_exceptions @requires_auth async def pkgbase_unvote(request: Request, name: str): pkgbase = get_pkg_or_base(name, PackageBase) - vote = pkgbase.package_votes.filter( - PackageVote.UsersID == request.user.ID - ).first() + vote = pkgbase.package_votes.filter(PackageVote.UsersID == request.user.ID).first() has_cred = request.user.has_credential(creds.PKGBASE_VOTE) if has_cred and vote: with db.begin(): @@ -497,97 +536,115 @@ async def pkgbase_unvote(request: Request, name: str): # Update NumVotes/Popularity. popupdate.run_single(pkgbase) - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/notify") @handle_form_exceptions @requires_auth async def pkgbase_notify(request: Request, name: str): pkgbase = get_pkg_or_base(name, PackageBase) actions.pkgbase_notify_instance(request, pkgbase) - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/unnotify") @handle_form_exceptions @requires_auth async def pkgbase_unnotify(request: Request, name: str): pkgbase = get_pkg_or_base(name, PackageBase) actions.pkgbase_unnotify_instance(request, pkgbase) - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/unflag") @handle_form_exceptions @requires_auth async def pkgbase_unflag(request: Request, name: str): pkgbase = get_pkg_or_base(name, PackageBase) actions.pkgbase_unflag_instance(request, pkgbase) - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) @router.get("/pkgbase/{name}/disown") @requires_auth -async def pkgbase_disown_get(request: Request, name: str, - next: str = Query(default=str())): +async def pkgbase_disown_get( + request: Request, name: str, next: str = Query(default=str()) +): pkgbase = get_pkg_or_base(name, PackageBase) - has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, - approved=[pkgbase.Maintainer]) + comaints = {c.User for c in pkgbase.comaintainers} + approved = [pkgbase.Maintainer] + list(comaints) + has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, approved=approved) if not has_cred: - return RedirectResponse(f"/pkgbase/{name}", - HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", HTTPStatus.SEE_OTHER) context = templates.make_context(request, "Disown Package") context["pkgbase"] = pkgbase context["next"] = next or "/pkgbase/{name}" + context["is_maint"] = request.user == pkgbase.Maintainer + context["is_comaint"] = request.user in comaints return render_template(request, "pkgbase/disown.html", context) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/disown") @handle_form_exceptions @requires_auth -async def pkgbase_disown_post(request: Request, name: str, - comments: str = Form(default=str()), - confirm: bool = Form(default=False), - next: str = Form(default=str())): +async def pkgbase_disown_post( + request: Request, + name: str, + comments: str = Form(default=str()), + confirm: bool = Form(default=False), + next: str = Form(default=str()), +): pkgbase = get_pkg_or_base(name, PackageBase) - has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, - approved=[pkgbase.Maintainer]) + if comments: + validate.comment_raise_http_ex(comments) + + comaints = {c.User for c in pkgbase.comaintainers} + approved = [pkgbase.Maintainer] + list(comaints) + has_cred = request.user.has_credential(creds.PKGBASE_DISOWN, approved=approved) if not has_cred: - return RedirectResponse(f"/pkgbase/{name}", - HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", HTTPStatus.SEE_OTHER) context = templates.make_context(request, "Disown Package") context["pkgbase"] = pkgbase - if not confirm: - context["errors"] = [("The selected packages have not been disowned, " - "check the confirmation checkbox.")] - return render_template(request, "pkgbase/disown.html", context, - status_code=HTTPStatus.BAD_REQUEST) + context["is_maint"] = request.user == pkgbase.Maintainer + context["is_comaint"] = request.user in comaints - with db.begin(): - update_closure_comment(pkgbase, ORPHAN_ID, comments) + if not confirm: + context["errors"] = [ + ( + "The selected packages have not been disowned, " + "check the confirmation checkbox." + ) + ] + return render_template( + request, "pkgbase/disown.html", context, status_code=HTTPStatus.BAD_REQUEST + ) + + if request.user != pkgbase.Maintainer and request.user not in comaints: + with db.begin(): + update_closure_comment(pkgbase, ORPHAN_ID, comments) try: actions.pkgbase_disown_instance(request, pkgbase) except InvariantError as exc: context["errors"] = [str(exc)] - return render_template(request, "pkgbase/disown.html", context, - status_code=HTTPStatus.BAD_REQUEST) - - if not next: - next = f"/pkgbase/{name}" + return render_template( + request, "pkgbase/disown.html", context, status_code=HTTPStatus.BAD_REQUEST + ) + next = next or f"/pkgbase/{name}" return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/adopt") @handle_form_exceptions @requires_auth @@ -601,8 +658,7 @@ async def pkgbase_adopt_post(request: Request, name: str): # if no maintainer currently exists. actions.pkgbase_adopt_instance(request, pkgbase) - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) @router.get("/pkgbase/{name}/comaintainers") @@ -613,71 +669,74 @@ async def pkgbase_comaintainers(request: Request, name: str) -> Response: # Unauthorized users (Non-TU/Dev and not the pkgbase maintainer) # get redirected to the package base's page. - has_creds = request.user.has_credential(creds.PKGBASE_EDIT_COMAINTAINERS, - approved=[pkgbase.Maintainer]) + has_creds = request.user.has_credential( + creds.PKGBASE_EDIT_COMAINTAINERS, approved=[pkgbase.Maintainer] + ) if not has_creds: - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) # Add our base information. context = templates.make_context(request, "Manage Co-maintainers") - context.update({ - "pkgbase": pkgbase, - "comaintainers": [ - c.User.Username for c in pkgbase.comaintainers - ] - }) + context.update( + { + "pkgbase": pkgbase, + "comaintainers": [c.User.Username for c in pkgbase.comaintainers], + } + ) return render_template(request, "pkgbase/comaintainers.html", context) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/comaintainers") @handle_form_exceptions @requires_auth -async def pkgbase_comaintainers_post(request: Request, name: str, - users: str = Form(default=str())) \ - -> Response: +async def pkgbase_comaintainers_post( + request: Request, name: str, users: str = Form(default=str()) +) -> Response: # Get the PackageBase. pkgbase = get_pkg_or_base(name, PackageBase) # Unauthorized users (Non-TU/Dev and not the pkgbase maintainer) # get redirected to the package base's page. - has_creds = request.user.has_credential(creds.PKGBASE_EDIT_COMAINTAINERS, - approved=[pkgbase.Maintainer]) + has_creds = request.user.has_credential( + creds.PKGBASE_EDIT_COMAINTAINERS, approved=[pkgbase.Maintainer] + ) if not has_creds: - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) users = {e.strip() for e in users.split("\n") if bool(e.strip())} records = {c.User.Username for c in pkgbase.comaintainers} users_to_rm = records.difference(users) pkgbaseutil.remove_comaintainers(pkgbase, users_to_rm) - logger.debug(f"{request.user} removed comaintainers from " - f"{pkgbase.Name}: {users_to_rm}") + logger.debug( + f"{request.user} removed comaintainers from " f"{pkgbase.Name}: {users_to_rm}" + ) users_to_add = users.difference(records) error = pkgbaseutil.add_comaintainers(request, pkgbase, users_to_add) if error: context = templates.make_context(request, "Manage Co-maintainers") context["pkgbase"] = pkgbase - context["comaintainers"] = [ - c.User.Username for c in pkgbase.comaintainers - ] + context["comaintainers"] = [c.User.Username for c in pkgbase.comaintainers] context["errors"] = [error] return render_template(request, "pkgbase/comaintainers.html", context) - logger.debug(f"{request.user} added comaintainers to " - f"{pkgbase.Name}: {users_to_add}") + logger.debug( + f"{request.user} added comaintainers to " f"{pkgbase.Name}: {users_to_add}" + ) - return RedirectResponse(f"/pkgbase/{pkgbase.Name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse( + f"/pkgbase/{pkgbase.Name}", status_code=HTTPStatus.SEE_OTHER + ) @router.get("/pkgbase/{name}/request") @requires_auth -async def pkgbase_request(request: Request, name: str, - next: str = Query(default=str())): +async def pkgbase_request( + request: Request, name: str, next: str = Query(default=str()) +): pkgbase = get_pkg_or_base(name, PackageBase) context = await make_variable_context(request, "Submit Request") context["pkgbase"] = pkgbase @@ -685,31 +744,32 @@ async def pkgbase_request(request: Request, name: str, return render_template(request, "pkgbase/request.html", context) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/request") @handle_form_exceptions @requires_auth -async def pkgbase_request_post(request: Request, name: str, - type: str = Form(...), - merge_into: str = Form(default=None), - comments: str = Form(default=str()), - next: str = Form(default=str())): +async def pkgbase_request_post( + request: Request, + name: str, + type: str = Form(...), + merge_into: str = Form(default=None), + comments: str = Form(default=str()), + next: str = Form(default=str()), +): pkgbase = get_pkg_or_base(name, PackageBase) # Create our render context. context = await make_variable_context(request, "Submit Request") context["pkgbase"] = pkgbase - types = { - "deletion": DELETION_ID, - "merge": MERGE_ID, - "orphan": ORPHAN_ID - } + types = {"deletion": DELETION_ID, "merge": MERGE_ID, "orphan": ORPHAN_ID} if type not in types: # In the case that someone crafted a POST request with an invalid # type, just return them to the request form with BAD_REQUEST status. - return render_template(request, "pkgbase/request.html", context, - status_code=HTTPStatus.BAD_REQUEST) + return render_template( + request, "pkgbase/request.html", context, status_code=HTTPStatus.BAD_REQUEST + ) try: validate.request(pkgbase, type, comments, merge_into, context) @@ -721,20 +781,26 @@ async def pkgbase_request_post(request: Request, name: str, # All good. Create a new PackageRequest based on the given type. now = time.utcnow() with db.begin(): - pkgreq = db.create(PackageRequest, - ReqTypeID=types.get(type), - User=request.user, - RequestTS=now, - PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - MergeBaseName=merge_into, - Comments=comments, - ClosureComment=str()) + pkgreq = db.create( + PackageRequest, + ReqTypeID=types.get(type), + User=request.user, + RequestTS=now, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + MergeBaseName=merge_into, + Comments=comments, + ClosureComment=str(), + ) # Prepare notification object. notif = notify.RequestOpenNotification( - request.user.ID, pkgreq.ID, type, - pkgreq.PackageBase.ID, merge_into=merge_into or None) + request.user.ID, + pkgreq.ID, + type, + pkgreq.PackageBase.ID, + merge_into=merge_into or None, + ) # Send the notification now that we're out of the DB scope. notif.send() @@ -753,13 +819,13 @@ async def pkgbase_request_post(request: Request, name: str, pkgbase.Maintainer = None pkgreq.Status = ACCEPTED_ID notif = notify.RequestCloseNotification( - request.user.ID, pkgreq.ID, pkgreq.status_display()) + request.user.ID, pkgreq.ID, pkgreq.status_display() + ) notif.send() logger.debug(f"New request #{pkgreq.ID} is marked for auto-orphan.") elif type == "deletion" and is_maintainer and outdated: # This request should be auto-accepted. - notifs = actions.pkgbase_delete_instance( - request, pkgbase, comments=comments) + notifs = actions.pkgbase_delete_instance(request, pkgbase, comments=comments) util.apply_all(notifs, lambda n: n.send()) logger.debug(f"New request #{pkgreq.ID} is marked for auto-deletion.") @@ -769,11 +835,11 @@ async def pkgbase_request_post(request: Request, name: str, @router.get("/pkgbase/{name}/delete") @requires_auth -async def pkgbase_delete_get(request: Request, name: str, - next: str = Query(default=str())): +async def pkgbase_delete_get( + request: Request, name: str, next: str = Query(default=str()) +): if not request.user.has_credential(creds.PKGBASE_DELETE): - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) context = templates.make_context(request, "Package Deletion") context["pkgbase"] = get_pkg_or_base(name, PackageBase) @@ -781,56 +847,65 @@ async def pkgbase_delete_get(request: Request, name: str, return render_template(request, "pkgbase/delete.html", context) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/delete") @handle_form_exceptions @requires_auth -async def pkgbase_delete_post(request: Request, name: str, - confirm: bool = Form(default=False), - comments: str = Form(default=str()), - next: str = Form(default="/packages")): +async def pkgbase_delete_post( + request: Request, + name: str, + confirm: bool = Form(default=False), + comments: str = Form(default=str()), + next: str = Form(default="/packages"), +): pkgbase = get_pkg_or_base(name, PackageBase) if not request.user.has_credential(creds.PKGBASE_DELETE): - return RedirectResponse(f"/pkgbase/{name}", - status_code=HTTPStatus.SEE_OTHER) + return RedirectResponse(f"/pkgbase/{name}", status_code=HTTPStatus.SEE_OTHER) if not confirm: context = templates.make_context(request, "Package Deletion") context["pkgbase"] = pkgbase - context["errors"] = [("The selected packages have not been deleted, " - "check the confirmation checkbox.")] - return render_template(request, "pkgbase/delete.html", context, - status_code=HTTPStatus.BAD_REQUEST) + context["errors"] = [ + ( + "The selected packages have not been deleted, " + "check the confirmation checkbox." + ) + ] + return render_template( + request, "pkgbase/delete.html", context, status_code=HTTPStatus.BAD_REQUEST + ) if comments: + validate.comment_raise_http_ex(comments) # Update any existing deletion requests' ClosureComment. with db.begin(): requests = pkgbase.requests.filter( - and_(PackageRequest.Status == PENDING_ID, - PackageRequest.ReqTypeID == DELETION_ID) + and_( + PackageRequest.Status == PENDING_ID, + PackageRequest.ReqTypeID == DELETION_ID, + ) ) for pkgreq in requests: pkgreq.ClosureComment = comments - notifs = actions.pkgbase_delete_instance( - request, pkgbase, comments=comments) + notifs = actions.pkgbase_delete_instance(request, pkgbase, comments=comments) util.apply_all(notifs, lambda n: n.send()) return RedirectResponse(next, status_code=HTTPStatus.SEE_OTHER) @router.get("/pkgbase/{name}/merge") @requires_auth -async def pkgbase_merge_get(request: Request, name: str, - into: str = Query(default=str()), - next: str = Query(default=str())): +async def pkgbase_merge_get( + request: Request, + name: str, + into: str = Query(default=str()), + next: str = Query(default=str()), +): pkgbase = get_pkg_or_base(name, PackageBase) context = templates.make_context(request, "Package Merging") - context.update({ - "pkgbase": pkgbase, - "into": into, - "next": next - }) + context.update({"pkgbase": pkgbase, "into": into, "next": next}) status_code = HTTPStatus.OK # TODO: Lookup errors from credential instead of hardcoding them. @@ -839,22 +914,27 @@ async def pkgbase_merge_get(request: Request, name: str, # Don't take these examples verbatim. We should find good naming. if not request.user.has_credential(creds.PKGBASE_MERGE): context["errors"] = [ - "Only Trusted Users and Developers can merge packages."] + "Only Package Maintainers and Developers can merge packages." + ] status_code = HTTPStatus.UNAUTHORIZED - return render_template(request, "pkgbase/merge.html", context, - status_code=status_code) + return render_template( + request, "pkgbase/merge.html", context, status_code=status_code + ) +@db.async_retry_deadlock @router.post("/pkgbase/{name}/merge") @handle_form_exceptions @requires_auth -async def pkgbase_merge_post(request: Request, name: str, - into: str = Form(default=str()), - comments: str = Form(default=str()), - confirm: bool = Form(default=False), - next: str = Form(default=str())): - +async def pkgbase_merge_post( + request: Request, + name: str, + into: str = Form(default=str()), + comments: str = Form(default=str()), + confirm: bool = Form(default=False), + next: str = Form(default=str()), +): pkgbase = get_pkg_or_base(name, PackageBase) context = await make_variable_context(request, "Package Merging") context["pkgbase"] = pkgbase @@ -862,28 +942,37 @@ async def pkgbase_merge_post(request: Request, name: str, # TODO: Lookup errors from credential instead of hardcoding them. if not request.user.has_credential(creds.PKGBASE_MERGE): context["errors"] = [ - "Only Trusted Users and Developers can merge packages."] - return render_template(request, "pkgbase/merge.html", context, - status_code=HTTPStatus.UNAUTHORIZED) + "Only Package Maintainers and Developers can merge packages." + ] + return render_template( + request, "pkgbase/merge.html", context, status_code=HTTPStatus.UNAUTHORIZED + ) if not confirm: - context["errors"] = ["The selected packages have not been deleted, " - "check the confirmation checkbox."] - return render_template(request, "pkgbase/merge.html", context, - status_code=HTTPStatus.BAD_REQUEST) + context["errors"] = [ + "The selected packages have not been deleted, " + "check the confirmation checkbox." + ] + return render_template( + request, "pkgbase/merge.html", context, status_code=HTTPStatus.BAD_REQUEST + ) try: target = get_pkg_or_base(into, PackageBase) except HTTPException: - context["errors"] = [ - "Cannot find package to merge votes and comments into."] - return render_template(request, "pkgbase/merge.html", context, - status_code=HTTPStatus.BAD_REQUEST) + context["errors"] = ["Cannot find package to merge votes and comments into."] + return render_template( + request, "pkgbase/merge.html", context, status_code=HTTPStatus.BAD_REQUEST + ) if pkgbase == target: context["errors"] = ["Cannot merge a package base with itself."] - return render_template(request, "pkgbase/merge.html", context, - status_code=HTTPStatus.BAD_REQUEST) + return render_template( + request, "pkgbase/merge.html", context, status_code=HTTPStatus.BAD_REQUEST + ) + + if comments: + validate.comment_raise_http_ex(comments) with db.begin(): update_closure_comment(pkgbase, MERGE_ID, comments, target=target) diff --git a/aurweb/routers/requests.py b/aurweb/routers/requests.py index 086aa3bc..a67419fe 100644 --- a/aurweb/routers/requests.py +++ b/aurweb/routers/requests.py @@ -2,57 +2,127 @@ from http import HTTPStatus from fastapi import APIRouter, Form, Query, Request from fastapi.responses import RedirectResponse -from sqlalchemy import case +from sqlalchemy import case, orm from aurweb import db, defaults, time, util from aurweb.auth import creds, requires_auth from aurweb.exceptions import handle_form_exceptions -from aurweb.models import PackageRequest, User -from aurweb.models.package_request import PENDING_ID, REJECTED_ID +from aurweb.models import PackageBase, PackageRequest, User +from aurweb.models.package_request import ( + ACCEPTED_ID, + CLOSED_ID, + PENDING_ID, + REJECTED_ID, +) from aurweb.requests.util import get_pkgreq_by_id from aurweb.scripts import notify +from aurweb.statistics import get_request_counts from aurweb.templates import make_context, render_template +FILTER_PARAMS = { + "filter_pending", + "filter_closed", + "filter_accepted", + "filter_rejected", + "filter_maintainers_requests", +} + router = APIRouter() @router.get("/requests") @requires_auth -async def requests(request: Request, - O: int = Query(default=defaults.O), - PP: int = Query(default=defaults.PP)): +async def requests( # noqa: C901 + request: Request, + O: int = Query(default=defaults.O), + PP: int = Query(default=defaults.PP), + filter_pending: bool = False, + filter_closed: bool = False, + filter_accepted: bool = False, + filter_rejected: bool = False, + filter_maintainer_requests: bool = False, + filter_pkg_name: str = None, +): context = make_context(request, "Requests") context["q"] = dict(request.query_params) - O, PP = util.sanitize_params(O, PP) + # Set pending filter by default if no status filter was provided. + # In case we got a package name filter, but no status filter, + # we enable the other ones too. + if not dict(request.query_params).keys() & FILTER_PARAMS: + filter_pending = True + if filter_pkg_name: + filter_closed = True + filter_accepted = True + filter_rejected = True + + O, PP = util.sanitize_params(str(O), str(PP)) context["O"] = O context["PP"] = PP + context["filter_pending"] = filter_pending + context["filter_closed"] = filter_closed + context["filter_accepted"] = filter_accepted + context["filter_rejected"] = filter_rejected + context["filter_maintainer_requests"] = filter_maintainer_requests + context["filter_pkg_name"] = filter_pkg_name - # A PackageRequest query, with left inner joined User and RequestType. - query = db.query(PackageRequest).join( - User, User.ID == PackageRequest.UsersID) + Maintainer = orm.aliased(User) + # A PackageRequest query + query = ( + db.query(PackageRequest) + .join(PackageBase) + .join(User, PackageRequest.UsersID == User.ID, isouter=True) + .join(Maintainer, PackageBase.MaintainerUID == Maintainer.ID, isouter=True) + ) + # Requests statistics + counts = get_request_counts() + for k in counts: + context[k] = counts[k] + + # Apply status filters + in_filters = [] + if filter_pending: + in_filters.append(PENDING_ID) + if filter_closed: + in_filters.append(CLOSED_ID) + if filter_accepted: + in_filters.append(ACCEPTED_ID) + if filter_rejected: + in_filters.append(REJECTED_ID) + filtered = query.filter(PackageRequest.Status.in_(in_filters)) + + # Name filter (contains) + if filter_pkg_name: + filtered = filtered.filter(PackageBase.Name.like(f"%{filter_pkg_name}%")) + + # Additionally filter for requests made from package maintainer + if filter_maintainer_requests: + filtered = filtered.filter(PackageRequest.UsersID == PackageBase.MaintainerUID) # If the request user is not elevated (TU or Dev), then # filter PackageRequests which are owned by the request user. if not request.user.is_elevated(): - query = query.filter(PackageRequest.UsersID == request.user.ID) - - context["total"] = query.count() - context["results"] = query.order_by( - # Order primarily by the Status column being PENDING_ID, - # and secondarily by RequestTS; both in descending order. - case([(PackageRequest.Status == PENDING_ID, 1)], else_=0).desc(), - PackageRequest.RequestTS.desc() - ).limit(PP).offset(O).all() + filtered = filtered.filter(PackageRequest.UsersID == request.user.ID) + context["total"] = filtered.count() + context["results"] = ( + filtered.order_by( + # Order primarily by the Status column being PENDING_ID, + # and secondarily by RequestTS; both in descending order. + case([(PackageRequest.Status == PENDING_ID, 1)], else_=0).desc(), + PackageRequest.RequestTS.desc(), + ) + .limit(PP) + .offset(O) + .all() + ) return render_template(request, "requests.html", context) @router.get("/requests/{id}/close") @requires_auth async def request_close(request: Request, id: int): - pkgreq = get_pkgreq_by_id(id) if not request.user.is_elevated() and request.user != pkgreq.User: # Request user doesn't have permission here: redirect to '/'. @@ -63,11 +133,13 @@ async def request_close(request: Request, id: int): return render_template(request, "requests/close.html", context) +@db.async_retry_deadlock @router.post("/requests/{id}/close") @handle_form_exceptions @requires_auth -async def request_close_post(request: Request, id: int, - comments: str = Form(default=str())): +async def request_close_post( + request: Request, id: int, comments: str = Form(default=str()) +): pkgreq = get_pkgreq_by_id(id) # `pkgreq`.User can close their own request. @@ -87,7 +159,8 @@ async def request_close_post(request: Request, id: int, pkgreq.Status = REJECTED_ID notify_ = notify.RequestCloseNotification( - request.user.ID, pkgreq.ID, pkgreq.status_display()) + request.user.ID, pkgreq.ID, pkgreq.status_display() + ) notify_.send() return RedirectResponse("/requests", status_code=HTTPStatus.SEE_OTHER) diff --git a/aurweb/routers/rpc.py b/aurweb/routers/rpc.py index 49e98f8c..645e6b5a 100644 --- a/aurweb/routers/rpc.py +++ b/aurweb/routers/rpc.py @@ -1,12 +1,36 @@ +""" +RPC API routing module + +For legacy route documentation, see https://aur.archlinux.org/rpc + +Legacy Routes: +- GET /rpc +- POST /rpc + +Legacy example (version 5): /rpc?v=5&type=info&arg=my-package + +For OpenAPI route documentation, see https://aur.archlinux.org/docs + +OpenAPI Routes: +- GET /rpc/v{version}/info/{arg} +- GET /rpc/v{version}/info +- POST /rpc/v{version}/info +- GET /rpc/v{version}/search/{arg} +- GET /rpc/v{version}/search +- POST /rpc/v{version}/search +- GET /rpc/v{version}/suggest/{arg} + +OpenAPI example (version 5): /rpc/v5/info/my-package + +""" + import hashlib import re - from http import HTTPStatus -from typing import List, Optional +from typing import Optional from urllib.parse import unquote import orjson - from fastapi import APIRouter, Form, Query, Request, Response from fastapi.responses import JSONResponse @@ -19,7 +43,7 @@ router = APIRouter() def parse_args(request: Request): - """ Handle legacy logic of 'arg' and 'arg[]' query parameter handling. + """Handle legacy logic of 'arg' and 'arg[]' query parameter handling. When 'arg' appears as the last argument given to the query string, that argument is used by itself as one single argument, regardless @@ -39,9 +63,7 @@ def parse_args(request: Request): # Create a list of (key, value) pairs of the given 'arg' and 'arg[]' # query parameters from last to first. query = list(reversed(unquote(request.url.query).split("&"))) - parts = [ - e.split("=", 1) for e in query if e.startswith(("arg=", "arg[]=")) - ] + parts = [e.split("=", 1) for e in query if e.startswith(("arg=", "arg[]="))] args = [] if parts: @@ -63,24 +85,27 @@ def parse_args(request: Request): return args -JSONP_EXPR = re.compile(r'^[a-zA-Z0-9()_.]{1,128}$') +JSONP_EXPR = re.compile(r"^[a-zA-Z0-9()_.]{1,128}$") -async def rpc_request(request: Request, - v: Optional[int] = None, - type: Optional[str] = None, - by: Optional[str] = defaults.RPC_SEARCH_BY, - arg: Optional[str] = None, - args: Optional[List[str]] = [], - callback: Optional[str] = None): - +async def rpc_request( + request: Request, + v: Optional[int] = None, + type: Optional[str] = None, + by: Optional[str] = defaults.RPC_SEARCH_BY, + arg: Optional[str] = None, + args: Optional[list[str]] = [], + callback: Optional[str] = None, +): # Create a handle to our RPC class. rpc = RPC(version=v, type=type) # If ratelimit was exceeded, return a 429 Too Many Requests. if check_ratelimit(request): - return JSONResponse(rpc.error("Rate limit reached"), - status_code=int(HTTPStatus.TOO_MANY_REQUESTS)) + return JSONResponse( + rpc.error("Rate limit reached"), + status_code=int(HTTPStatus.TOO_MANY_REQUESTS), + ) # If `callback` was provided, produce a text/javascript response # valid for the jsonp callback. Otherwise, by default, return @@ -115,15 +140,11 @@ async def rpc_request(request: Request, # The ETag header expects quotes to surround any identifier. # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag - headers = { - "Content-Type": content_type, - "ETag": f'"{etag}"' - } + headers = {"Content-Type": content_type, "ETag": f'"{etag}"'} if_none_match = request.headers.get("If-None-Match", str()) - if if_none_match and if_none_match.strip("\t\n\r\" ") == etag: - return Response(headers=headers, - status_code=int(HTTPStatus.NOT_MODIFIED)) + if if_none_match and if_none_match.strip('\t\n\r" ') == etag: + return Response(headers=headers, status_code=int(HTTPStatus.NOT_MODIFIED)) if callback: content = f"/**/{callback}({content.decode()})" @@ -135,13 +156,15 @@ async def rpc_request(request: Request, @router.get("/rpc.php") # Temporary! Remove on 03/04 @router.get("/rpc/") @router.get("/rpc") -async def rpc(request: Request, - v: Optional[int] = Query(default=None), - type: Optional[str] = Query(default=None), - by: Optional[str] = Query(default=defaults.RPC_SEARCH_BY), - arg: Optional[str] = Query(default=None), - args: Optional[List[str]] = Query(default=[], alias="arg[]"), - callback: Optional[str] = Query(default=None)): +async def rpc( + request: Request, + v: Optional[int] = Query(default=None), + type: Optional[str] = Query(default=None), + by: Optional[str] = Query(default=defaults.RPC_SEARCH_BY), + arg: Optional[str] = Query(default=None), + args: Optional[list[str]] = Query(default=[], alias="arg[]"), + callback: Optional[str] = Query(default=None), +): if not request.url.query: return documentation() return await rpc_request(request, v, type, by, arg, args, callback) @@ -152,11 +175,146 @@ async def rpc(request: Request, @router.post("/rpc/") @router.post("/rpc") @handle_form_exceptions -async def rpc_post(request: Request, - v: Optional[int] = Form(default=None), - type: Optional[str] = Form(default=None), - by: Optional[str] = Form(default=defaults.RPC_SEARCH_BY), - arg: Optional[str] = Form(default=None), - args: Optional[List[str]] = Form(default=[], alias="arg[]"), - callback: Optional[str] = Form(default=None)): +async def rpc_post( + request: Request, + v: Optional[int] = Form(default=None), + type: Optional[str] = Form(default=None), + by: Optional[str] = Form(default=defaults.RPC_SEARCH_BY), + arg: Optional[str] = Form(default=None), + args: list[str] = Form(default=[], alias="arg[]"), + callback: Optional[str] = Form(default=None), +): return await rpc_request(request, v, type, by, arg, args, callback) + + +@router.get("/rpc/v{version}/info/{name}") +async def rpc_openapi_info(request: Request, version: int, name: str): + return await rpc_request( + request, + version, + "info", + defaults.RPC_SEARCH_BY, + name, + [], + ) + + +@router.get("/rpc/v{version}/info") +async def rpc_openapi_multiinfo( + request: Request, + version: int, + args: Optional[list[str]] = Query(default=[], alias="arg[]"), +): + arg = args.pop(0) if args else None + return await rpc_request( + request, + version, + "info", + defaults.RPC_SEARCH_BY, + arg, + args, + ) + + +@router.post("/rpc/v{version}/info") +async def rpc_openapi_multiinfo_post( + request: Request, + version: int, +): + data = await request.json() + + args = data.get("arg", []) + if not isinstance(args, list): + rpc = RPC(version, "info") + return JSONResponse( + rpc.error("the 'arg' parameter must be of array type"), + status_code=HTTPStatus.BAD_REQUEST, + ) + + arg = args.pop(0) if args else None + return await rpc_request( + request, + version, + "info", + defaults.RPC_SEARCH_BY, + arg, + args, + ) + + +@router.get("/rpc/v{version}/search/{arg}") +async def rpc_openapi_search_arg( + request: Request, + version: int, + arg: str, + by: Optional[str] = Query(default=defaults.RPC_SEARCH_BY), +): + return await rpc_request( + request, + version, + "search", + by, + arg, + [], + ) + + +@router.get("/rpc/v{version}/search") +async def rpc_openapi_search( + request: Request, + version: int, + arg: Optional[str] = Query(default=str()), + by: Optional[str] = Query(default=defaults.RPC_SEARCH_BY), +): + return await rpc_request( + request, + version, + "search", + by, + arg, + [], + ) + + +@router.post("/rpc/v{version}/search") +async def rpc_openapi_search_post( + request: Request, + version: int, +): + data = await request.json() + by = data.get("by", defaults.RPC_SEARCH_BY) + if not isinstance(by, str): + rpc = RPC(version, "search") + return JSONResponse( + rpc.error("the 'by' parameter must be of string type"), + status_code=HTTPStatus.BAD_REQUEST, + ) + + arg = data.get("arg", str()) + if not isinstance(arg, str): + rpc = RPC(version, "search") + return JSONResponse( + rpc.error("the 'arg' parameter must be of string type"), + status_code=HTTPStatus.BAD_REQUEST, + ) + + return await rpc_request( + request, + version, + "search", + by, + arg, + [], + ) + + +@router.get("/rpc/v{version}/suggest/{arg}") +async def rpc_openapi_suggest(request: Request, version: int, arg: str): + return await rpc_request( + request, + version, + "suggest", + defaults.RPC_SEARCH_BY, + arg, + [], + ) diff --git a/aurweb/routers/rss.py b/aurweb/routers/rss.py index 0996f3cd..180cbb23 100644 --- a/aurweb/routers/rss.py +++ b/aurweb/routers/rss.py @@ -1,22 +1,19 @@ -from datetime import datetime - from fastapi import APIRouter, Request from fastapi.responses import Response from feedgen.feed import FeedGenerator -from aurweb import db, filters +from aurweb import config, db, filters +from aurweb.cache import lambda_cache from aurweb.models import Package, PackageBase router = APIRouter() -def make_rss_feed(request: Request, packages: list, - date_attr: str): - """ Create an RSS Feed string for some packages. +def make_rss_feed(request: Request, packages: list): + """Create an RSS Feed string for some packages. :param request: A FastAPI request :param packages: A list of packages to add to the RSS feed - :param date_attr: The date attribute (DB column) to use :return: RSS Feed string """ @@ -26,58 +23,67 @@ def make_rss_feed(request: Request, packages: list, base = f"{request.url.scheme}://{request.url.netloc}" feed.link(href=base, rel="alternate") feed.link(href=f"{base}/rss", rel="self") - feed.image(title="AUR Newest Packages", - url=f"{base}/static/css/archnavbar/aurlogo.png", - link=base, - description="AUR Newest Packages Feed") + feed.image( + title="AUR Newest Packages", + url=f"{base}/static/css/archnavbar/aurlogo.png", + link=base, + description="AUR Newest Packages Feed", + ) for pkg in packages: entry = feed.add_entry(order="append") entry.title(pkg.Name) entry.link(href=f"{base}/packages/{pkg.Name}", rel="alternate") - entry.link(href=f"{base}/rss", rel="self", type="application/rss+xml") entry.description(pkg.Description or str()) - - attr = getattr(pkg.PackageBase, date_attr) - dt = filters.timestamp_to_datetime(attr) + dt = filters.timestamp_to_datetime(pkg.Timestamp) dt = filters.as_timezone(dt, request.user.Timezone) entry.pubDate(dt.strftime("%Y-%m-%d %H:%M:%S%z")) - - entry.source(f"{base}") - if pkg.PackageBase.Maintainer: - entry.author(author={"name": pkg.PackageBase.Maintainer.Username}) - entry.guid(f"{pkg.Name} - {attr}") + entry.guid(f"{pkg.Name}-{pkg.Timestamp}") return feed.rss_str() @router.get("/rss/") async def rss(request: Request): - packages = db.query(Package).join(PackageBase).order_by( - PackageBase.SubmittedTS.desc()).limit(100) - feed = make_rss_feed(request, packages, "SubmittedTS") + packages = ( + db.query(Package) + .join(PackageBase) + .order_by(PackageBase.SubmittedTS.desc()) + .limit(100) + .with_entities( + Package.Name, + Package.Description, + PackageBase.SubmittedTS.label("Timestamp"), + ) + ) + + # we use redis for caching the results of the feedgen + cache_expire = config.getint("cache", "expiry_time_rss", 300) + feed = lambda_cache("rss", lambda: make_rss_feed(request, packages), cache_expire) response = Response(feed, media_type="application/rss+xml") - package = packages.first() - if package: - dt = datetime.utcfromtimestamp(package.PackageBase.SubmittedTS) - modified = dt.strftime("%a, %d %m %Y %H:%M:%S GMT") - response.headers["Last-Modified"] = modified - return response @router.get("/rss/modified") async def rss_modified(request: Request): - packages = db.query(Package).join(PackageBase).order_by( - PackageBase.ModifiedTS.desc()).limit(100) - feed = make_rss_feed(request, packages, "ModifiedTS") + packages = ( + db.query(Package) + .join(PackageBase) + .order_by(PackageBase.ModifiedTS.desc()) + .limit(100) + .with_entities( + Package.Name, + Package.Description, + PackageBase.ModifiedTS.label("Timestamp"), + ) + ) + + # we use redis for caching the results of the feedgen + cache_expire = config.getint("cache", "expiry_time_rss", 300) + feed = lambda_cache( + "rss_modified", lambda: make_rss_feed(request, packages), cache_expire + ) response = Response(feed, media_type="application/rss+xml") - package = packages.first() - if package: - dt = datetime.utcfromtimestamp(package.PackageBase.ModifiedTS) - modified = dt.strftime("%a, %d %m %Y %H:%M:%S GMT") - response.headers["Last-Modified"] = modified - return response diff --git a/aurweb/routers/sso.py b/aurweb/routers/sso.py index eff1c63f..fb99edd6 100644 --- a/aurweb/routers/sso.py +++ b/aurweb/routers/sso.py @@ -1,11 +1,9 @@ import time import uuid - from http import HTTPStatus from urllib.parse import urlencode import fastapi - from authlib.integrations.starlette_client import OAuth, OAuthError from fastapi import Depends, HTTPException from fastapi.responses import RedirectResponse @@ -14,7 +12,6 @@ from starlette.requests import Request import aurweb.config import aurweb.db - from aurweb import util from aurweb.l10n import get_translator_for_request from aurweb.schema import Bans, Sessions, Users @@ -43,14 +40,18 @@ async def login(request: Request, redirect: str = None): The `redirect` argument is a query parameter specifying the post-login redirect URL. """ - authenticate_url = aurweb.config.get("options", "aur_location") + "/sso/authenticate" + authenticate_url = ( + aurweb.config.get("options", "aur_location") + "/sso/authenticate" + ) if redirect: authenticate_url = authenticate_url + "?" + urlencode([("redirect", redirect)]) return await oauth.sso.authorize_redirect(request, authenticate_url, prompt="login") def is_account_suspended(conn, user_id): - row = conn.execute(select([Users.c.Suspended]).where(Users.c.ID == user_id)).fetchone() + row = conn.execute( + select([Users.c.Suspended]).where(Users.c.ID == user_id) + ).fetchone() return row is not None and bool(row[0]) @@ -60,23 +61,29 @@ def open_session(request, conn, user_id): """ if is_account_suspended(conn, user_id): _ = get_translator_for_request(request) - raise HTTPException(status_code=HTTPStatus.FORBIDDEN, - detail=_('Account suspended')) + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, detail=_("Account suspended") + ) # TODO This is a terrible message because it could imply the attempt at # logging in just caused the suspension. sid = uuid.uuid4().hex - conn.execute(Sessions.insert().values( - UsersID=user_id, - SessionID=sid, - LastUpdateTS=time.time(), - )) + conn.execute( + Sessions.insert().values( + UsersID=user_id, + SessionID=sid, + LastUpdateTS=time.time(), + ) + ) # Update user’s last login information. - conn.execute(Users.update() - .where(Users.c.ID == user_id) - .values(LastLogin=int(time.time()), - LastLoginIPAddress=request.client.host)) + conn.execute( + Users.update() + .where(Users.c.ID == user_id) + .values( + LastLogin=int(time.time()), LastLoginIPAddress=util.get_client_ip(request) + ) + ) return sid @@ -98,18 +105,23 @@ def is_aur_url(url): @router.get("/sso/authenticate") -async def authenticate(request: Request, redirect: str = None, conn=Depends(aurweb.db.connect)): +async def authenticate( + request: Request, redirect: str = None, conn=Depends(aurweb.db.connect) +): """ Receive an OpenID Connect ID token, validate it, then process it to create an new AUR session. """ - if is_ip_banned(conn, request.client.host): + if is_ip_banned(conn, util.get_client_ip(request)): _ = get_translator_for_request(request) raise HTTPException( status_code=HTTPStatus.FORBIDDEN, - detail=_('The login form is currently disabled for your IP address, ' - 'probably due to sustained spam attacks. Sorry for the ' - 'inconvenience.')) + detail=_( + "The login form is currently disabled for your IP address, " + "probably due to sustained spam attacks. Sorry for the " + "inconvenience." + ), + ) try: token = await oauth.sso.authorize_access_token(request) @@ -120,30 +132,41 @@ async def authenticate(request: Request, redirect: str = None, conn=Depends(aurw _ = get_translator_for_request(request) raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, - detail=_('Bad OAuth token. Please retry logging in from the start.')) + detail=_("Bad OAuth token. Please retry logging in from the start."), + ) sub = user.get("sub") # this is the SSO account ID in JWT terminology if not sub: _ = get_translator_for_request(request) - raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, - detail=_("JWT is missing its `sub` field.")) + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=_("JWT is missing its `sub` field."), + ) - aur_accounts = conn.execute(select([Users.c.ID]).where(Users.c.SSOAccountID == sub)) \ - .fetchall() + aur_accounts = conn.execute( + select([Users.c.ID]).where(Users.c.SSOAccountID == sub) + ).fetchall() if not aur_accounts: return "Sorry, we don’t seem to know you Sir " + sub elif len(aur_accounts) == 1: sid = open_session(request, conn, aur_accounts[0][Users.c.ID]) - response = RedirectResponse(redirect if redirect and is_aur_url(redirect) else "/") + response = RedirectResponse( + redirect if redirect and is_aur_url(redirect) else "/" + ) secure_cookies = aurweb.config.getboolean("options", "disable_http_login") - response.set_cookie(key="AURSID", value=sid, httponly=True, - secure=secure_cookies) + response.set_cookie( + key="AURSID", value=sid, httponly=True, secure=secure_cookies + ) if "id_token" in token: # We save the id_token for the SSO logout. It’s not too important # though, so if we can’t find it, we can live without it. - response.set_cookie(key="SSO_ID_TOKEN", value=token["id_token"], - path="/sso/", httponly=True, - secure=secure_cookies) + response.set_cookie( + key="SSO_ID_TOKEN", + value=token["id_token"], + path="/sso/", + httponly=True, + secure=secure_cookies, + ) return util.add_samesite_fields(response, "strict") else: # We’ve got a severe integrity violation. @@ -165,8 +188,12 @@ async def logout(request: Request): return RedirectResponse("/") metadata = await oauth.sso.load_server_metadata() - query = urlencode({'post_logout_redirect_uri': aurweb.config.get('options', 'aur_location'), - 'id_token_hint': id_token}) - response = RedirectResponse(metadata["end_session_endpoint"] + '?' + query) + query = urlencode( + { + "post_logout_redirect_uri": aurweb.config.get("options", "aur_location"), + "id_token_hint": id_token, + } + ) + response = RedirectResponse(metadata["end_session_endpoint"] + "?" + query) response.delete_cookie("SSO_ID_TOKEN", path="/sso/") return response diff --git a/aurweb/routers/trusted_user.py b/aurweb/routers/trusted_user.py deleted file mode 100644 index cbe3e47d..00000000 --- a/aurweb/routers/trusted_user.py +++ /dev/null @@ -1,319 +0,0 @@ -import html -import typing - -from http import HTTPStatus - -from fastapi import APIRouter, Form, HTTPException, Request -from fastapi.responses import RedirectResponse, Response -from sqlalchemy import and_, func, or_ - -from aurweb import db, l10n, logging, models, time -from aurweb.auth import creds, requires_auth -from aurweb.exceptions import handle_form_exceptions -from aurweb.models import User -from aurweb.models.account_type import TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID -from aurweb.templates import make_context, make_variable_context, render_template - -router = APIRouter() -logger = logging.get_logger(__name__) - -# Some TU route specific constants. -ITEMS_PER_PAGE = 10 # Paged table size. -MAX_AGENDA_LENGTH = 75 # Agenda table column length. - -ADDVOTE_SPECIFICS = { - # This dict stores a vote duration and quorum for a proposal. - # When a proposal is added, duration is added to the current - # timestamp. - # "addvote_type": (duration, quorum) - "add_tu": (7 * 24 * 60 * 60, 0.66), - "remove_tu": (7 * 24 * 60 * 60, 0.75), - "remove_inactive_tu": (5 * 24 * 60 * 60, 0.66), - "bylaws": (7 * 24 * 60 * 60, 0.75) -} - - -@router.get("/tu") -@requires_auth -async def trusted_user(request: Request, - coff: int = 0, # current offset - cby: str = "desc", # current by - poff: int = 0, # past offset - pby: str = "desc"): # past by - if not request.user.has_credential(creds.TU_LIST_VOTES): - return RedirectResponse("/", status_code=HTTPStatus.SEE_OTHER) - - context = make_context(request, "Trusted User") - - current_by, past_by = cby, pby - current_off, past_off = coff, poff - - context["pp"] = pp = ITEMS_PER_PAGE - context["prev_len"] = MAX_AGENDA_LENGTH - - ts = time.utcnow() - - if current_by not in {"asc", "desc"}: - # If a malicious by was given, default to desc. - current_by = "desc" - context["current_by"] = current_by - - if past_by not in {"asc", "desc"}: - # If a malicious by was given, default to desc. - past_by = "desc" - context["past_by"] = past_by - - current_votes = db.query(models.TUVoteInfo).filter( - models.TUVoteInfo.End > ts).order_by( - models.TUVoteInfo.Submitted.desc()) - context["current_votes_count"] = current_votes.count() - current_votes = current_votes.limit(pp).offset(current_off) - context["current_votes"] = reversed(current_votes.all()) \ - if current_by == "asc" else current_votes.all() - context["current_off"] = current_off - - past_votes = db.query(models.TUVoteInfo).filter( - models.TUVoteInfo.End <= ts).order_by( - models.TUVoteInfo.Submitted.desc()) - context["past_votes_count"] = past_votes.count() - past_votes = past_votes.limit(pp).offset(past_off) - context["past_votes"] = reversed(past_votes.all()) \ - if past_by == "asc" else past_votes.all() - context["past_off"] = past_off - - last_vote = func.max(models.TUVote.VoteID).label("LastVote") - last_votes_by_tu = db.query(models.TUVote).join(models.User).join( - models.TUVoteInfo, - models.TUVoteInfo.ID == models.TUVote.VoteID - ).filter( - and_(models.TUVote.VoteID == models.TUVoteInfo.ID, - models.User.ID == models.TUVote.UserID, - models.TUVoteInfo.End < ts, - or_(models.User.AccountTypeID == 2, - models.User.AccountTypeID == 4)) - ).with_entities( - models.TUVote.UserID, - last_vote, - models.User.Username - ).group_by(models.TUVote.UserID).order_by( - last_vote.desc(), models.User.Username.asc()) - context["last_votes_by_tu"] = last_votes_by_tu.all() - - context["current_by_next"] = "asc" if current_by == "desc" else "desc" - context["past_by_next"] = "asc" if past_by == "desc" else "desc" - - context["q"] = { - "coff": current_off, - "cby": current_by, - "poff": past_off, - "pby": past_by - } - - return render_template(request, "tu/index.html", context) - - -def render_proposal(request: Request, context: dict, proposal: int, - voteinfo: models.TUVoteInfo, - voters: typing.Iterable[models.User], - vote: models.TUVote, - status_code: HTTPStatus = HTTPStatus.OK): - """ Render a single TU proposal. """ - context["proposal"] = proposal - context["voteinfo"] = voteinfo - context["voters"] = voters.all() - - total = voteinfo.total_votes() - participation = (total / voteinfo.ActiveTUs) if voteinfo.ActiveTUs else 0 - context["participation"] = participation - - accepted = (voteinfo.Yes > voteinfo.ActiveTUs / 2) or \ - (participation > voteinfo.Quorum and voteinfo.Yes > voteinfo.No) - context["accepted"] = accepted - - can_vote = voters.filter(models.TUVote.User == request.user).first() is None - context["can_vote"] = can_vote - - if not voteinfo.is_running(): - context["error"] = "Voting is closed for this proposal." - - context["vote"] = vote - context["has_voted"] = vote is not None - - return render_template(request, "tu/show.html", context, - status_code=status_code) - - -@router.get("/tu/{proposal}") -@requires_auth -async def trusted_user_proposal(request: Request, proposal: int): - if not request.user.has_credential(creds.TU_LIST_VOTES): - return RedirectResponse("/tu", status_code=HTTPStatus.SEE_OTHER) - - context = await make_variable_context(request, "Trusted User") - proposal = int(proposal) - - voteinfo = db.query(models.TUVoteInfo).filter( - models.TUVoteInfo.ID == proposal).first() - if not voteinfo: - raise HTTPException(status_code=HTTPStatus.NOT_FOUND) - - voters = db.query(models.User).join(models.TUVote).filter( - models.TUVote.VoteID == voteinfo.ID) - vote = db.query(models.TUVote).filter( - and_(models.TUVote.UserID == request.user.ID, - models.TUVote.VoteID == voteinfo.ID)).first() - if not request.user.has_credential(creds.TU_VOTE): - context["error"] = "Only Trusted Users are allowed to vote." - if voteinfo.User == request.user.Username: - context["error"] = "You cannot vote in an proposal about you." - elif vote is not None: - context["error"] = "You've already voted for this proposal." - - context["vote"] = vote - return render_proposal(request, context, proposal, voteinfo, voters, vote) - - -@router.post("/tu/{proposal}") -@handle_form_exceptions -@requires_auth -async def trusted_user_proposal_post(request: Request, proposal: int, - decision: str = Form(...)): - if not request.user.has_credential(creds.TU_LIST_VOTES): - return RedirectResponse("/tu", status_code=HTTPStatus.SEE_OTHER) - - context = await make_variable_context(request, "Trusted User") - proposal = int(proposal) # Make sure it's an int. - - voteinfo = db.query(models.TUVoteInfo).filter( - models.TUVoteInfo.ID == proposal).first() - if not voteinfo: - raise HTTPException(status_code=HTTPStatus.NOT_FOUND) - - voters = db.query(models.User).join(models.TUVote).filter( - models.TUVote.VoteID == voteinfo.ID) - vote = db.query(models.TUVote).filter( - and_(models.TUVote.UserID == request.user.ID, - models.TUVote.VoteID == voteinfo.ID)).first() - - status_code = HTTPStatus.OK - if not request.user.has_credential(creds.TU_VOTE): - context["error"] = "Only Trusted Users are allowed to vote." - status_code = HTTPStatus.UNAUTHORIZED - elif voteinfo.User == request.user.Username: - context["error"] = "You cannot vote in an proposal about you." - status_code = HTTPStatus.BAD_REQUEST - elif vote is not None: - context["error"] = "You've already voted for this proposal." - status_code = HTTPStatus.BAD_REQUEST - - if status_code != HTTPStatus.OK: - return render_proposal(request, context, proposal, - voteinfo, voters, vote, - status_code=status_code) - - if decision in {"Yes", "No", "Abstain"}: - # Increment whichever decision was given to us. - setattr(voteinfo, decision, getattr(voteinfo, decision) + 1) - else: - return Response("Invalid 'decision' value.", - status_code=HTTPStatus.BAD_REQUEST) - - with db.begin(): - vote = db.create(models.TUVote, User=request.user, VoteInfo=voteinfo) - - context["error"] = "You've already voted for this proposal." - return render_proposal(request, context, proposal, voteinfo, voters, vote) - - -@router.get("/addvote") -@requires_auth -async def trusted_user_addvote(request: Request, user: str = str(), - type: str = "add_tu", agenda: str = str()): - if not request.user.has_credential(creds.TU_ADD_VOTE): - return RedirectResponse("/tu", status_code=HTTPStatus.SEE_OTHER) - - context = await make_variable_context(request, "Add Proposal") - - if type not in ADDVOTE_SPECIFICS: - context["error"] = "Invalid type." - type = "add_tu" # Default it. - - context["user"] = user - context["type"] = type - context["agenda"] = agenda - - return render_template(request, "addvote.html", context) - - -@router.post("/addvote") -@handle_form_exceptions -@requires_auth -async def trusted_user_addvote_post(request: Request, - user: str = Form(default=str()), - type: str = Form(default=str()), - agenda: str = Form(default=str())): - if not request.user.has_credential(creds.TU_ADD_VOTE): - return RedirectResponse("/tu", status_code=HTTPStatus.SEE_OTHER) - - # Build a context. - context = await make_variable_context(request, "Add Proposal") - - context["type"] = type - context["user"] = user - context["agenda"] = agenda - - def render_addvote(context, status_code): - """ Simplify render_template a bit for this test. """ - return render_template(request, "addvote.html", context, status_code) - - # Alright, get some database records, if we can. - if type != "bylaws": - user_record = db.query(models.User).filter( - models.User.Username == user).first() - if user_record is None: - context["error"] = "Username does not exist." - return render_addvote(context, HTTPStatus.NOT_FOUND) - - utcnow = time.utcnow() - voteinfo = db.query(models.TUVoteInfo).filter( - and_(models.TUVoteInfo.User == user, - models.TUVoteInfo.End > utcnow)).count() - if voteinfo: - _ = l10n.get_translator_for_request(request) - context["error"] = _( - "%s already has proposal running for them.") % ( - html.escape(user),) - return render_addvote(context, HTTPStatus.BAD_REQUEST) - - if type not in ADDVOTE_SPECIFICS: - context["error"] = "Invalid type." - context["type"] = type = "add_tu" # Default for rendering. - return render_addvote(context, HTTPStatus.BAD_REQUEST) - - if not agenda: - context["error"] = "Proposal cannot be empty." - return render_addvote(context, HTTPStatus.BAD_REQUEST) - - # Gather some mapped constants and the current timestamp. - duration, quorum = ADDVOTE_SPECIFICS.get(type) - timestamp = time.utcnow() - - # Active TU types we filter for. - types = {TRUSTED_USER_ID, TRUSTED_USER_AND_DEV_ID} - - # Create a new TUVoteInfo (proposal)! - with db.begin(): - active_tus = db.query(User).filter( - and_(User.Suspended == 0, - User.InactivityTS.isnot(None), - User.AccountTypeID.in_(types)) - ).count() - voteinfo = db.create(models.TUVoteInfo, User=user, - Agenda=html.escape(agenda), - Submitted=timestamp, End=(timestamp + duration), - Quorum=quorum, ActiveTUs=active_tus, - Submitter=request.user) - - # Redirect to the new proposal. - endpoint = f"/tu/{voteinfo.ID}" - return RedirectResponse(endpoint, status_code=HTTPStatus.SEE_OTHER) diff --git a/aurweb/rpc.py b/aurweb/rpc.py index 5bc6b80d..5fcbbb78 100644 --- a/aurweb/rpc.py +++ b/aurweb/rpc.py @@ -1,16 +1,15 @@ import os - from collections import defaultdict -from typing import Any, Callable, Dict, List, NewType, Union +from typing import Any, Callable, NewType, Union from fastapi.responses import HTMLResponse from sqlalchemy import and_, literal, orm import aurweb.config as config - -from aurweb import db, defaults, models +from aurweb import db, defaults, models, time from aurweb.exceptions import RPCError from aurweb.filters import number_format +from aurweb.models.package_base import popularity from aurweb.packages.search import RPCSearch TYPE_MAPPING = { @@ -23,8 +22,7 @@ TYPE_MAPPING = { "replaces": "Replaces", } -DataGenerator = NewType("DataGenerator", - Callable[[models.Package], Dict[str, Any]]) +DataGenerator = NewType("DataGenerator", Callable[[models.Package], dict[str, Any]]) def documentation(): @@ -40,7 +38,7 @@ def documentation(): class RPC: - """ RPC API handler class. + """RPC API handler class. There are various pieces to RPC's process, and encapsulating them inside of a class means that external users do not abuse the @@ -66,36 +64,58 @@ class RPC: # A set of RPC types supported by this API. EXPOSED_TYPES = { - "info", "multiinfo", - "search", "msearch", - "suggest", "suggest-pkgbase" + "info", + "multiinfo", + "search", + "msearch", + "suggest", + "suggest-pkgbase", } # A mapping of type aliases. TYPE_ALIASES = {"info": "multiinfo"} EXPOSED_BYS = { - "name-desc", "name", "maintainer", - "depends", "makedepends", "optdepends", "checkdepends" + "name-desc", + "name", + "maintainer", + "depends", + "makedepends", + "optdepends", + "checkdepends", + "provides", + "conflicts", + "replaces", + "groups", + "submitter", + "keywords", + "comaintainers", } # A mapping of by aliases. - BY_ALIASES = {"name-desc": "nd", "name": "n", "maintainer": "m"} + BY_ALIASES = { + "name-desc": "nd", + "name": "n", + "maintainer": "m", + "submitter": "s", + "keywords": "k", + "comaintainers": "c", + } def __init__(self, version: int = 0, type: str = None) -> "RPC": self.version = version self.type = RPC.TYPE_ALIASES.get(type, type) - def error(self, message: str) -> Dict[str, Any]: + def error(self, message: str) -> dict[str, Any]: return { "version": self.version, "results": [], "resultcount": 0, "type": "error", - "error": message + "error": message, } - def _verify_inputs(self, by: str = [], args: List[str] = []) -> None: + def _verify_inputs(self, by: str = [], args: list[str] = []) -> None: if self.version is None: raise RPCError("Please specify an API version.") @@ -111,20 +131,19 @@ class RPC: if self.type not in RPC.EXPOSED_TYPES: raise RPCError("Incorrect request type specified.") - def _enforce_args(self, args: List[str]) -> None: + def _enforce_args(self, args: list[str]) -> None: if not args: raise RPCError("No request type/data specified.") - def _get_json_data(self, package: models.Package) -> Dict[str, Any]: - """ Produce dictionary data of one Package that can be JSON-serialized. + def get_json_data(self, package: models.Package) -> dict[str, Any]: + """Produce dictionary data of one Package that can be JSON-serialized. :param package: Package instance :returns: JSON-serializable dictionary """ - # Produce RPC API compatible Popularity: If zero, it's an integer - # 0, otherwise, it's formatted to the 6th decimal place. - pop = package.Popularity + # Normalize Popularity for RPC output to 6 decimal precision + pop = popularity(package, time.utcnow()) pop = 0 if not pop else float(number_format(pop, 6)) snapshot_uri = config.get("options", "snapshot_uri") @@ -135,26 +154,24 @@ class RPC: "PackageBase": package.PackageBaseName, # Maintainer should be set following this update if one exists. "Maintainer": package.Maintainer, + "Submitter": package.Submitter, "Version": package.Version, "Description": package.Description, "URL": package.URL, - "URLPath": snapshot_uri % package.Name, + "URLPath": snapshot_uri % package.PackageBaseName, "NumVotes": package.NumVotes, "Popularity": pop, "OutOfDate": package.OutOfDateTS, "FirstSubmitted": package.SubmittedTS, - "LastModified": package.ModifiedTS + "LastModified": package.ModifiedTS, } - def _get_info_json_data(self, package: models.Package) -> Dict[str, Any]: - data = self._get_json_data(package) + def get_info_json_data(self, package: models.Package) -> dict[str, Any]: + data = self.get_json_data(package) # All info results have _at least_ an empty list of # License and Keywords. - data.update({ - "License": [], - "Keywords": [] - }) + data.update({"License": [], "Keywords": []}) # If we actually got extra_info records, update data with # them for this particular package. @@ -163,9 +180,9 @@ class RPC: return data - def _assemble_json_data(self, packages: List[models.Package], - data_generator: DataGenerator) \ - -> List[Dict[str, Any]]: + def _assemble_json_data( + self, packages: list[models.Package], data_generator: DataGenerator + ) -> list[dict[str, Any]]: """ Assemble JSON data out of a list of packages. @@ -174,108 +191,129 @@ class RPC: """ return [data_generator(pkg) for pkg in packages] - def _entities(self, query: orm.Query) -> orm.Query: - """ Select specific RPC columns on `query`. """ - return query.with_entities( - models.Package.ID, - models.Package.Name, - models.Package.Version, - models.Package.Description, - models.Package.URL, - models.Package.PackageBaseID, - models.PackageBase.Name.label("PackageBaseName"), - models.PackageBase.NumVotes, - models.PackageBase.Popularity, - models.PackageBase.OutOfDateTS, - models.PackageBase.SubmittedTS, - models.PackageBase.ModifiedTS, - models.User.Username.label("Maintainer"), - ).group_by(models.Package.ID) + def entities(self, query: orm.Query) -> orm.Query: + """Select specific RPC columns on `query`.""" + Submitter = orm.aliased(models.User) - def _handle_multiinfo_type(self, args: List[str] = [], **kwargs) \ - -> List[Dict[str, Any]]: - self._enforce_args(args) - args = set(args) + query = ( + query.join( + Submitter, + Submitter.ID == models.PackageBase.SubmitterUID, + isouter=True, + ) + .with_entities( + models.Package.ID, + models.Package.Name, + models.Package.Version, + models.Package.Description, + models.Package.URL, + models.Package.PackageBaseID, + models.PackageBase.Name.label("PackageBaseName"), + models.PackageBase.NumVotes, + models.PackageBase.Popularity, + models.PackageBase.PopularityUpdated, + models.PackageBase.OutOfDateTS, + models.PackageBase.SubmittedTS, + models.PackageBase.ModifiedTS, + models.User.Username.label("Maintainer"), + Submitter.Username.label("Submitter"), + ) + .group_by(models.Package.ID) + ) - packages = db.query(models.Package).join(models.PackageBase).join( - models.User, - models.User.ID == models.PackageBase.MaintainerUID, - isouter=True - ).filter(models.Package.Name.in_(args)) + return query - max_results = config.getint("options", "max_rpc_results") - packages = self._entities(packages).limit(max_results + 1) - - if packages.count() > max_results: - raise RPCError("Too many package results.") - - ids = {pkg.ID for pkg in packages} - - # Aliases for 80-width. + def subquery(self, ids: set[int]): Package = models.Package PackageKeyword = models.PackageKeyword subqueries = [ # PackageDependency - db.query( - models.PackageDependency - ).join(models.DependencyType).filter( - models.PackageDependency.PackageID.in_(ids) - ).with_entities( + db.query(models.PackageDependency) + .join(models.DependencyType) + .filter(models.PackageDependency.PackageID.in_(ids)) + .with_entities( models.PackageDependency.PackageID.label("ID"), models.DependencyType.Name.label("Type"), models.PackageDependency.DepName.label("Name"), - models.PackageDependency.DepCondition.label("Cond") - ).distinct().order_by("Name"), - + models.PackageDependency.DepCondition.label("Cond"), + ) + .distinct() + .order_by("Name"), # PackageRelation - db.query( - models.PackageRelation - ).join(models.RelationType).filter( - models.PackageRelation.PackageID.in_(ids) - ).with_entities( + db.query(models.PackageRelation) + .join(models.RelationType) + .filter(models.PackageRelation.PackageID.in_(ids)) + .with_entities( models.PackageRelation.PackageID.label("ID"), models.RelationType.Name.label("Type"), models.PackageRelation.RelName.label("Name"), - models.PackageRelation.RelCondition.label("Cond") - ).distinct().order_by("Name"), - + models.PackageRelation.RelCondition.label("Cond"), + ) + .distinct() + .order_by("Name"), # Groups - db.query(models.PackageGroup).join( + db.query(models.PackageGroup) + .join( models.Group, - and_(models.PackageGroup.GroupID == models.Group.ID, - models.PackageGroup.PackageID.in_(ids)) - ).with_entities( + and_( + models.PackageGroup.GroupID == models.Group.ID, + models.PackageGroup.PackageID.in_(ids), + ), + ) + .with_entities( models.PackageGroup.PackageID.label("ID"), literal("Groups").label("Type"), models.Group.Name.label("Name"), - literal(str()).label("Cond") - ).distinct().order_by("Name"), - + literal(str()).label("Cond"), + ) + .distinct() + .order_by("Name"), # Licenses - db.query(models.PackageLicense).join( - models.License, - models.PackageLicense.LicenseID == models.License.ID - ).filter( - models.PackageLicense.PackageID.in_(ids) - ).with_entities( + db.query(models.PackageLicense) + .join(models.License, models.PackageLicense.LicenseID == models.License.ID) + .filter(models.PackageLicense.PackageID.in_(ids)) + .with_entities( models.PackageLicense.PackageID.label("ID"), literal("License").label("Type"), models.License.Name.label("Name"), - literal(str()).label("Cond") - ).distinct().order_by("Name"), - + literal(str()).label("Cond"), + ) + .distinct() + .order_by("Name"), # Keywords - db.query(models.PackageKeyword).join( + db.query(models.PackageKeyword) + .join( models.Package, - and_(Package.PackageBaseID == PackageKeyword.PackageBaseID, - Package.ID.in_(ids)) - ).with_entities( + and_( + Package.PackageBaseID == PackageKeyword.PackageBaseID, + Package.ID.in_(ids), + ), + ) + .with_entities( models.Package.ID.label("ID"), literal("Keywords").label("Type"), models.PackageKeyword.Keyword.label("Name"), - literal(str()).label("Cond") - ).distinct().order_by("Name") + literal(str()).label("Cond"), + ) + .distinct() + .order_by("Name"), + # Co-Maintainer + db.query(models.PackageComaintainer) + .join(models.User, models.User.ID == models.PackageComaintainer.UsersID) + .join( + models.Package, + models.Package.PackageBaseID + == models.PackageComaintainer.PackageBaseID, + ) + .with_entities( + models.Package.ID, + literal("CoMaintainers").label("Type"), + models.User.Username.label("Name"), + literal(str()).label("Cond"), + ) + .distinct() # A package could have the same co-maintainer multiple times + .order_by("Name"), ] # Union all subqueries together. @@ -293,10 +331,37 @@ class RPC: self.extra_info[record.ID][type_].append(name) - return self._assemble_json_data(packages, self._get_info_json_data) + def _handle_multiinfo_type( + self, args: list[str] = [], **kwargs + ) -> list[dict[str, Any]]: + self._enforce_args(args) + args = set(args) - def _handle_search_type(self, by: str = defaults.RPC_SEARCH_BY, - args: List[str] = []) -> List[Dict[str, Any]]: + packages = ( + db.query(models.Package) + .join(models.PackageBase) + .join( + models.User, + models.User.ID == models.PackageBase.MaintainerUID, + isouter=True, + ) + .filter(models.Package.Name.in_(args)) + ) + + max_results = config.getint("options", "max_rpc_results") + packages = self.entities(packages).limit(max_results + 1) + + if packages.count() > max_results: + raise RPCError("Too many package results.") + + ids = {pkg.ID for pkg in packages} + self.subquery(ids) + + return self._assemble_json_data(packages, self.get_info_json_data) + + def _handle_search_type( + self, by: str = defaults.RPC_SEARCH_BY, args: list[str] = [] + ) -> list[dict[str, Any]]: # If `by` isn't maintainer and we don't have any args, raise an error. # In maintainer's case, return all orphans if there are no args, # so we need args to pass through to the handler without errors. @@ -311,57 +376,77 @@ class RPC: search.search_by(by, arg) max_results = config.getint("options", "max_rpc_results") - results = self._entities(search.results()).limit(max_results + 1).all() + query = self.entities(search.results()).limit(max_results + 1) + + # For "provides", we need to union our relation search + # with an exact search since a package always provides itself. + # Turns out that doing this with an OR statement is extremely slow + if by == "provides": + search = RPCSearch() + search._search_by_exact_name(arg) + query = query.union(self.entities(search.results())) + + results = query.all() if len(results) > max_results: raise RPCError("Too many package results.") - return self._assemble_json_data(results, self._get_json_data) + data = self._assemble_json_data(results, self.get_json_data) - def _handle_msearch_type(self, args: List[str] = [], **kwargs)\ - -> List[Dict[str, Any]]: + # remove Submitter for search results + for pkg in data: + pkg.pop("Submitter") + + return data + + def _handle_msearch_type( + self, args: list[str] = [], **kwargs + ) -> list[dict[str, Any]]: return self._handle_search_type(by="m", args=args) - def _handle_suggest_type(self, args: List[str] = [], **kwargs)\ - -> List[str]: + def _handle_suggest_type(self, args: list[str] = [], **kwargs) -> list[str]: if not args: return [] arg = args[0] - packages = db.query(models.Package.Name).join( - models.PackageBase - ).filter( - and_(models.PackageBase.PackagerUID.isnot(None), - models.Package.Name.like(f"{arg}%")) - ).order_by(models.Package.Name.asc()).limit(20) + packages = ( + db.query(models.Package.Name) + .join(models.PackageBase) + .filter(models.Package.Name.like(f"{arg}%")) + .order_by(models.Package.Name.asc()) + .limit(20) + ) return [pkg.Name for pkg in packages] - def _handle_suggest_pkgbase_type(self, args: List[str] = [], **kwargs)\ - -> List[str]: + def _handle_suggest_pkgbase_type(self, args: list[str] = [], **kwargs) -> list[str]: if not args: return [] arg = args[0] - packages = db.query(models.PackageBase.Name).filter( - and_(models.PackageBase.PackagerUID.isnot(None), - models.PackageBase.Name.like(f"{arg}%")) - ).order_by(models.PackageBase.Name.asc()).limit(20) + packages = ( + db.query(models.PackageBase.Name) + .filter(models.PackageBase.Name.like(f"{arg}%")) + .order_by(models.PackageBase.Name.asc()) + .limit(20) + ) return [pkg.Name for pkg in packages] def _is_suggestion(self) -> bool: return self.type.startswith("suggest") - def _handle_callback(self, by: str, args: List[str])\ - -> Union[List[Dict[str, Any]], List[str]]: + def _handle_callback( + self, by: str, args: list[str] + ) -> Union[list[dict[str, Any]], list[str]]: # Get a handle to our callback and trap an RPCError with # an empty list of results based on callback's execution. callback = getattr(self, f"_handle_{self.type.replace('-', '_')}_type") results = callback(by=by, args=args) return results - def handle(self, by: str = defaults.RPC_SEARCH_BY, args: List[str] = [])\ - -> Union[List[Dict[str, Any]], Dict[str, Any]]: - """ Request entrypoint. A router should pass v, type and args + def handle( + self, by: str = defaults.RPC_SEARCH_BY, args: list[str] = [] + ) -> Union[list[dict[str, Any]], dict[str, Any]]: + """Request entrypoint. A router should pass v, type and args to this function and expect an output dictionary to be returned. :param v: RPC version argument @@ -392,8 +477,5 @@ class RPC: return results # Return JSON output. - data.update({ - "resultcount": len(results), - "results": results - }) + data.update({"resultcount": len(results), "results": results}) return data diff --git a/aurweb/schema.py b/aurweb/schema.py index d2644541..76fd6556 100644 --- a/aurweb/schema.py +++ b/aurweb/schema.py @@ -5,8 +5,18 @@ Changes here should always be accompanied by an Alembic migration, which can be usually be automatically generated. See `migrations/README` for details. """ - -from sqlalchemy import CHAR, TIMESTAMP, Column, ForeignKey, Index, MetaData, String, Table, Text, text +from sqlalchemy import ( + CHAR, + TIMESTAMP, + Column, + ForeignKey, + Index, + MetaData, + String, + Table, + Text, + text, +) from sqlalchemy.dialects.mysql import BIGINT, DECIMAL, INTEGER, TINYINT from sqlalchemy.ext.compiler import compiles @@ -15,13 +25,13 @@ import aurweb.config db_backend = aurweb.config.get("database", "backend") -@compiles(TINYINT, 'sqlite') +@compiles(TINYINT, "sqlite") def compile_tinyint_sqlite(type_, compiler, **kw): # pragma: no cover """TINYINT is not supported on SQLite. Substitute it with INTEGER.""" - return 'INTEGER' + return "INTEGER" -@compiles(BIGINT, 'sqlite') +@compiles(BIGINT, "sqlite") def compile_bigint_sqlite(type_, compiler, **kw): # pragma: no cover """ For SQLite's AUTOINCREMENT to work on BIGINT columns, we need to map BIGINT @@ -29,429 +39,585 @@ def compile_bigint_sqlite(type_, compiler, **kw): # pragma: no cover See https://docs.sqlalchemy.org/en/13/dialects/sqlite.html#allowing-autoincrement-behavior-sqlalchemy-types-other-than-integer-integer """ # noqa: E501 - return 'INTEGER' + return "INTEGER" metadata = MetaData() # Define the Account Types for the AUR. AccountTypes = Table( - 'AccountTypes', metadata, - Column('ID', TINYINT(unsigned=True), primary_key=True), - Column('AccountType', String(32), nullable=False, server_default=text("''")), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci' + "AccountTypes", + metadata, + Column("ID", TINYINT(unsigned=True), primary_key=True), + Column("AccountType", String(32), nullable=False, server_default=text("''")), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # User information for each user regardless of type. Users = Table( - 'Users', metadata, - Column('ID', INTEGER(unsigned=True), primary_key=True), - Column('AccountTypeID', ForeignKey('AccountTypes.ID', ondelete="NO ACTION"), nullable=False, server_default=text("1")), - Column('Suspended', TINYINT(unsigned=True), nullable=False, server_default=text("0")), - Column('Username', String(32), nullable=False, unique=True), - Column('Email', String(254), nullable=False, unique=True), - Column('BackupEmail', String(254)), - Column('HideEmail', TINYINT(unsigned=True), nullable=False, server_default=text("0")), - Column('Passwd', String(255), nullable=False), - Column('Salt', CHAR(32), nullable=False, server_default=text("''")), - Column('ResetKey', CHAR(32), nullable=False, server_default=text("''")), - Column('RealName', String(64), nullable=False, server_default=text("''")), - Column('LangPreference', String(6), nullable=False, server_default=text("'en'")), - Column('Timezone', String(32), nullable=False, server_default=text("'UTC'")), - Column('Homepage', Text), - Column('IRCNick', String(32), nullable=False, server_default=text("''")), - Column('PGPKey', String(40)), - Column('LastLogin', BIGINT(unsigned=True), nullable=False, server_default=text("0")), - Column('LastLoginIPAddress', String(45)), - Column('LastSSHLogin', BIGINT(unsigned=True), nullable=False, server_default=text("0")), - Column('LastSSHLoginIPAddress', String(45)), - Column('InactivityTS', BIGINT(unsigned=True), nullable=False, server_default=text("0")), - Column('RegistrationTS', TIMESTAMP, nullable=False, server_default=text("CURRENT_TIMESTAMP")), - Column('CommentNotify', TINYINT(1), nullable=False, server_default=text("1")), - Column('UpdateNotify', TINYINT(1), nullable=False, server_default=text("0")), - Column('OwnershipNotify', TINYINT(1), nullable=False, server_default=text("1")), - Column('SSOAccountID', String(255), nullable=True, unique=True), - Index('UsersAccountTypeID', 'AccountTypeID'), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "Users", + metadata, + Column("ID", INTEGER(unsigned=True), primary_key=True), + Column( + "AccountTypeID", + ForeignKey("AccountTypes.ID", ondelete="NO ACTION"), + nullable=False, + server_default=text("1"), + ), + Column( + "Suspended", TINYINT(unsigned=True), nullable=False, server_default=text("0") + ), + Column("Username", String(32), nullable=False, unique=True), + Column("Email", String(254), nullable=False, unique=True), + Column("BackupEmail", String(254)), + Column( + "HideEmail", TINYINT(unsigned=True), nullable=False, server_default=text("0") + ), + Column("Passwd", String(255), nullable=False), + Column("Salt", CHAR(32), nullable=False, server_default=text("''")), + Column("ResetKey", CHAR(32), nullable=False, server_default=text("''")), + Column("RealName", String(64), nullable=False, server_default=text("''")), + Column("LangPreference", String(6), nullable=False, server_default=text("'en'")), + Column("Timezone", String(32), nullable=False, server_default=text("'UTC'")), + Column("Homepage", Text), + Column("IRCNick", String(32), nullable=False, server_default=text("''")), + Column("PGPKey", String(40)), + Column( + "LastLogin", BIGINT(unsigned=True), nullable=False, server_default=text("0") + ), + Column("LastLoginIPAddress", String(45)), + Column( + "LastSSHLogin", BIGINT(unsigned=True), nullable=False, server_default=text("0") + ), + Column("LastSSHLoginIPAddress", String(45)), + Column( + "InactivityTS", BIGINT(unsigned=True), nullable=False, server_default=text("0") + ), + Column( + "RegistrationTS", + TIMESTAMP, + nullable=False, + server_default=text("CURRENT_TIMESTAMP"), + ), + Column("CommentNotify", TINYINT(1), nullable=False, server_default=text("1")), + Column("UpdateNotify", TINYINT(1), nullable=False, server_default=text("0")), + Column("OwnershipNotify", TINYINT(1), nullable=False, server_default=text("1")), + Column("SSOAccountID", String(255), nullable=True, unique=True), + Index("UsersAccountTypeID", "AccountTypeID"), + Column( + "HideDeletedComments", + TINYINT(unsigned=True), + nullable=False, + server_default=text("0"), + ), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # SSH public keys used for the aurweb SSH/Git interface. SSHPubKeys = Table( - 'SSHPubKeys', metadata, - Column('UserID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), - Column('Fingerprint', String(44), primary_key=True), - Column('PubKey', String(4096), nullable=False), - mysql_engine='InnoDB', mysql_charset='utf8mb4', mysql_collate='utf8mb4_bin', + "SSHPubKeys", + metadata, + Column("UserID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), + Column("Fingerprint", String(44), primary_key=True), + Column("PubKey", String(4096), nullable=False), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_bin", ) # Track Users logging in/out of AUR web site. Sessions = Table( - 'Sessions', metadata, - Column('UsersID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), - Column('SessionID', CHAR(32), nullable=False, unique=True), - Column('LastUpdateTS', BIGINT(unsigned=True), nullable=False), - mysql_engine='InnoDB', mysql_charset='utf8mb4', mysql_collate='utf8mb4_bin', + "Sessions", + metadata, + Column("UsersID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), + Column("SessionID", CHAR(32), nullable=False, unique=True), + Column("LastUpdateTS", BIGINT(unsigned=True), nullable=False), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_bin", ) # Information on package bases PackageBases = Table( - 'PackageBases', metadata, - Column('ID', INTEGER(unsigned=True), primary_key=True), - Column('Name', String(255), nullable=False, unique=True), - Column('NumVotes', INTEGER(unsigned=True), nullable=False, server_default=text("0")), - Column('Popularity', - DECIMAL(10, 6, unsigned=True) - if db_backend == "mysql" else String(17), - nullable=False, server_default=text("0")), - Column('OutOfDateTS', BIGINT(unsigned=True)), - Column('FlaggerComment', Text, nullable=False), - Column('SubmittedTS', BIGINT(unsigned=True), nullable=False), - Column('ModifiedTS', BIGINT(unsigned=True), nullable=False), - Column('FlaggerUID', ForeignKey('Users.ID', ondelete='SET NULL')), # who flagged the package out-of-date? + "PackageBases", + metadata, + Column("ID", INTEGER(unsigned=True), primary_key=True), + Column("Name", String(255), nullable=False, unique=True), + Column( + "NumVotes", INTEGER(unsigned=True), nullable=False, server_default=text("0") + ), + Column( + "Popularity", + DECIMAL(10, 6, unsigned=True) if db_backend == "mysql" else String(17), + nullable=False, + server_default=text("0"), + ), + Column( + "PopularityUpdated", + TIMESTAMP, + nullable=False, + server_default=text("'1970-01-01 00:00:01.000000'"), + ), + Column("OutOfDateTS", BIGINT(unsigned=True)), + Column("FlaggerComment", Text, nullable=False), + Column("SubmittedTS", BIGINT(unsigned=True), nullable=False), + Column("ModifiedTS", BIGINT(unsigned=True), nullable=False), + Column( + "FlaggerUID", ForeignKey("Users.ID", ondelete="SET NULL") + ), # who flagged the package out-of-date? # deleting a user will cause packages to be orphaned, not deleted - Column('SubmitterUID', ForeignKey('Users.ID', ondelete='SET NULL')), # who submitted it? - Column('MaintainerUID', ForeignKey('Users.ID', ondelete='SET NULL')), # User - Column('PackagerUID', ForeignKey('Users.ID', ondelete='SET NULL')), # Last packager - Index('BasesMaintainerUID', 'MaintainerUID'), - Index('BasesNumVotes', 'NumVotes'), - Index('BasesPackagerUID', 'PackagerUID'), - Index('BasesSubmitterUID', 'SubmitterUID'), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + Column( + "SubmitterUID", ForeignKey("Users.ID", ondelete="SET NULL") + ), # who submitted it? + Column("MaintainerUID", ForeignKey("Users.ID", ondelete="SET NULL")), # User + Column("PackagerUID", ForeignKey("Users.ID", ondelete="SET NULL")), # Last packager + Index("BasesMaintainerUID", "MaintainerUID"), + Index("BasesNumVotes", "NumVotes"), + Index("BasesPackagerUID", "PackagerUID"), + Index("BasesSubmitterUID", "SubmitterUID"), + Index("BasesSubmittedTS", "SubmittedTS"), + Index("BasesModifiedTS", "ModifiedTS"), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Keywords of package bases PackageKeywords = Table( - 'PackageKeywords', metadata, - Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), primary_key=True, nullable=True), - Column('Keyword', String(255), primary_key=True, nullable=False, server_default=text("''")), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "PackageKeywords", + metadata, + Column( + "PackageBaseID", + ForeignKey("PackageBases.ID", ondelete="CASCADE"), + primary_key=True, + nullable=True, + ), + Column( + "Keyword", + String(255), + primary_key=True, + nullable=False, + server_default=text("''"), + ), + Index("KeywordsPackageBaseID", "PackageBaseID"), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Information about the actual packages Packages = Table( - 'Packages', metadata, - Column('ID', INTEGER(unsigned=True), primary_key=True), - Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), nullable=False), - Column('Name', String(255), nullable=False, unique=True), - Column('Version', String(255), nullable=False, server_default=text("''")), - Column('Description', String(255)), - Column('URL', String(8000)), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "Packages", + metadata, + Column("ID", INTEGER(unsigned=True), primary_key=True), + Column( + "PackageBaseID", + ForeignKey("PackageBases.ID", ondelete="CASCADE"), + nullable=False, + ), + Column("Name", String(255), nullable=False, unique=True), + Column("Version", String(255), nullable=False, server_default=text("''")), + Column("Description", String(255)), + Column("URL", String(8000)), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Information about licenses Licenses = Table( - 'Licenses', metadata, - Column('ID', INTEGER(unsigned=True), primary_key=True), - Column('Name', String(255), nullable=False, unique=True), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "Licenses", + metadata, + Column("ID", INTEGER(unsigned=True), primary_key=True), + Column("Name", String(255), nullable=False, unique=True), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Information about package-license-relations PackageLicenses = Table( - 'PackageLicenses', metadata, - Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), primary_key=True, nullable=True), - Column('LicenseID', ForeignKey('Licenses.ID', ondelete='CASCADE'), primary_key=True, nullable=True), - mysql_engine='InnoDB', + "PackageLicenses", + metadata, + Column( + "PackageID", + ForeignKey("Packages.ID", ondelete="CASCADE"), + primary_key=True, + nullable=True, + ), + Column( + "LicenseID", + ForeignKey("Licenses.ID", ondelete="CASCADE"), + primary_key=True, + nullable=True, + ), + mysql_engine="InnoDB", ) # Information about groups Groups = Table( - 'Groups', metadata, - Column('ID', INTEGER(unsigned=True), primary_key=True), - Column('Name', String(255), nullable=False, unique=True), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "Groups", + metadata, + Column("ID", INTEGER(unsigned=True), primary_key=True), + Column("Name", String(255), nullable=False, unique=True), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Information about package-group-relations PackageGroups = Table( - 'PackageGroups', metadata, - Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), primary_key=True, nullable=True), - Column('GroupID', ForeignKey('Groups.ID', ondelete='CASCADE'), primary_key=True, nullable=True), - mysql_engine='InnoDB', + "PackageGroups", + metadata, + Column( + "PackageID", + ForeignKey("Packages.ID", ondelete="CASCADE"), + primary_key=True, + nullable=True, + ), + Column( + "GroupID", + ForeignKey("Groups.ID", ondelete="CASCADE"), + primary_key=True, + nullable=True, + ), + mysql_engine="InnoDB", ) # Define the package dependency types DependencyTypes = Table( - 'DependencyTypes', metadata, - Column('ID', TINYINT(unsigned=True), primary_key=True), - Column('Name', String(32), nullable=False, server_default=text("''")), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "DependencyTypes", + metadata, + Column("ID", TINYINT(unsigned=True), primary_key=True), + Column("Name", String(32), nullable=False, server_default=text("''")), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Track which dependencies a package has PackageDepends = Table( - 'PackageDepends', metadata, - Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), nullable=False), - Column('DepTypeID', ForeignKey('DependencyTypes.ID', ondelete="NO ACTION"), nullable=False), - Column('DepName', String(255), nullable=False), - Column('DepDesc', String(255)), - Column('DepCondition', String(255)), - Column('DepArch', String(255)), - Index('DependsDepName', 'DepName'), - Index('DependsPackageID', 'PackageID'), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "PackageDepends", + metadata, + Column("PackageID", ForeignKey("Packages.ID", ondelete="CASCADE"), nullable=False), + Column( + "DepTypeID", + ForeignKey("DependencyTypes.ID", ondelete="NO ACTION"), + nullable=False, + ), + Column("DepName", String(255), nullable=False), + Column("DepDesc", String(255)), + Column("DepCondition", String(255)), + Column("DepArch", String(255)), + Index("DependsDepName", "DepName"), + Index("DependsPackageID", "PackageID"), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Define the package relation types RelationTypes = Table( - 'RelationTypes', metadata, - Column('ID', TINYINT(unsigned=True), primary_key=True), - Column('Name', String(32), nullable=False, server_default=text("''")), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "RelationTypes", + metadata, + Column("ID", TINYINT(unsigned=True), primary_key=True), + Column("Name", String(32), nullable=False, server_default=text("''")), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Track which conflicts, provides and replaces a package has PackageRelations = Table( - 'PackageRelations', metadata, - Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), nullable=False), - Column('RelTypeID', ForeignKey('RelationTypes.ID', ondelete="NO ACTION"), nullable=False), - Column('RelName', String(255), nullable=False), - Column('RelCondition', String(255)), - Column('RelArch', String(255)), - Index('RelationsPackageID', 'PackageID'), - Index('RelationsRelName', 'RelName'), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "PackageRelations", + metadata, + Column("PackageID", ForeignKey("Packages.ID", ondelete="CASCADE"), nullable=False), + Column( + "RelTypeID", + ForeignKey("RelationTypes.ID", ondelete="NO ACTION"), + nullable=False, + ), + Column("RelName", String(255), nullable=False), + Column("RelCondition", String(255)), + Column("RelArch", String(255)), + Index("RelationsPackageID", "PackageID"), + Index("RelationsRelName", "RelName"), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Track which sources a package has PackageSources = Table( - 'PackageSources', metadata, - Column('PackageID', ForeignKey('Packages.ID', ondelete='CASCADE'), nullable=False), - Column('Source', String(8000), nullable=False, server_default=text("'/dev/null'")), - Column('SourceArch', String(255)), - Index('SourcesPackageID', 'PackageID'), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "PackageSources", + metadata, + Column("PackageID", ForeignKey("Packages.ID", ondelete="CASCADE"), nullable=False), + Column("Source", String(8000), nullable=False, server_default=text("'/dev/null'")), + Column("SourceArch", String(255)), + Index("SourcesPackageID", "PackageID"), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Track votes for packages PackageVotes = Table( - 'PackageVotes', metadata, - Column('UsersID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), - Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), nullable=False), - Column('VoteTS', BIGINT(unsigned=True), nullable=False), - Index('VoteUsersIDPackageID', 'UsersID', 'PackageBaseID', unique=True), - Index('VotesPackageBaseID', 'PackageBaseID'), - Index('VotesUsersID', 'UsersID'), - mysql_engine='InnoDB', + "PackageVotes", + metadata, + Column("UsersID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), + Column( + "PackageBaseID", + ForeignKey("PackageBases.ID", ondelete="CASCADE"), + nullable=False, + ), + Column("VoteTS", BIGINT(unsigned=True), nullable=False), + Index("VoteUsersIDPackageID", "UsersID", "PackageBaseID", unique=True), + Index("VotesPackageBaseID", "PackageBaseID"), + Index("VotesUsersID", "UsersID"), + mysql_engine="InnoDB", ) # Record comments for packages PackageComments = Table( - 'PackageComments', metadata, - Column('ID', BIGINT(unsigned=True), primary_key=True), - Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), nullable=False), - Column('UsersID', ForeignKey('Users.ID', ondelete='SET NULL')), - Column('Comments', Text, nullable=False), - Column('RenderedComment', Text, nullable=False), - Column('CommentTS', BIGINT(unsigned=True), nullable=False, server_default=text("0")), - Column('EditedTS', BIGINT(unsigned=True)), - Column('EditedUsersID', ForeignKey('Users.ID', ondelete='SET NULL')), - Column('DelTS', BIGINT(unsigned=True)), - Column('DelUsersID', ForeignKey('Users.ID', ondelete='CASCADE')), - Column('PinnedTS', BIGINT(unsigned=True), nullable=False, server_default=text("0")), - Index('CommentsPackageBaseID', 'PackageBaseID'), - Index('CommentsUsersID', 'UsersID'), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "PackageComments", + metadata, + Column("ID", BIGINT(unsigned=True), primary_key=True), + Column( + "PackageBaseID", + ForeignKey("PackageBases.ID", ondelete="CASCADE"), + nullable=False, + ), + Column("UsersID", ForeignKey("Users.ID", ondelete="SET NULL")), + Column("Comments", Text, nullable=False), + Column("RenderedComment", Text, nullable=False), + Column( + "CommentTS", BIGINT(unsigned=True), nullable=False, server_default=text("0") + ), + Column("EditedTS", BIGINT(unsigned=True)), + Column("EditedUsersID", ForeignKey("Users.ID", ondelete="SET NULL")), + Column("DelTS", BIGINT(unsigned=True)), + Column("DelUsersID", ForeignKey("Users.ID", ondelete="CASCADE")), + Column("PinnedTS", BIGINT(unsigned=True), nullable=False, server_default=text("0")), + Index("CommentsPackageBaseID", "PackageBaseID"), + Index("CommentsUsersID", "UsersID"), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Package base co-maintainers PackageComaintainers = Table( - 'PackageComaintainers', metadata, - Column('UsersID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), - Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), nullable=False), - Column('Priority', INTEGER(unsigned=True), nullable=False), - Index('ComaintainersPackageBaseID', 'PackageBaseID'), - Index('ComaintainersUsersID', 'UsersID'), - mysql_engine='InnoDB', + "PackageComaintainers", + metadata, + Column("UsersID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), + Column( + "PackageBaseID", + ForeignKey("PackageBases.ID", ondelete="CASCADE"), + nullable=False, + ), + Column("Priority", INTEGER(unsigned=True), nullable=False), + Index("ComaintainersPackageBaseID", "PackageBaseID"), + Index("ComaintainersUsersID", "UsersID"), + mysql_engine="InnoDB", ) # Package base notifications PackageNotifications = Table( - 'PackageNotifications', metadata, - Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='CASCADE'), nullable=False), - Column('UserID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), - Index('NotifyUserIDPkgID', 'UserID', 'PackageBaseID', unique=True), - mysql_engine='InnoDB', + "PackageNotifications", + metadata, + Column( + "PackageBaseID", + ForeignKey("PackageBases.ID", ondelete="CASCADE"), + nullable=False, + ), + Column("UserID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), + Index("NotifyUserIDPkgID", "UserID", "PackageBaseID", unique=True), + mysql_engine="InnoDB", ) # Package name blacklist PackageBlacklist = Table( - 'PackageBlacklist', metadata, - Column('ID', INTEGER(unsigned=True), primary_key=True), - Column('Name', String(64), nullable=False, unique=True), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "PackageBlacklist", + metadata, + Column("ID", INTEGER(unsigned=True), primary_key=True), + Column("Name", String(64), nullable=False, unique=True), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Providers in the official repositories OfficialProviders = Table( - 'OfficialProviders', metadata, - Column('ID', INTEGER(unsigned=True), primary_key=True), - Column('Name', String(64), nullable=False), - Column('Repo', String(64), nullable=False), - Column('Provides', String(64), nullable=False), - Index('ProviderNameProvides', 'Name', 'Provides', unique=True), - mysql_engine='InnoDB', mysql_charset='utf8mb4', mysql_collate='utf8mb4_bin', + "OfficialProviders", + metadata, + Column("ID", INTEGER(unsigned=True), primary_key=True), + Column("Name", String(64), nullable=False), + Column("Repo", String(64), nullable=False), + Column("Provides", String(64), nullable=False), + Index("ProviderNameProvides", "Name", "Provides", unique=True), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_bin", ) # Define package request types RequestTypes = Table( - 'RequestTypes', metadata, - Column('ID', TINYINT(unsigned=True), primary_key=True), - Column('Name', String(32), nullable=False, server_default=text("''")), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "RequestTypes", + metadata, + Column("ID", TINYINT(unsigned=True), primary_key=True), + Column("Name", String(32), nullable=False, server_default=text("''")), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Package requests PackageRequests = Table( - 'PackageRequests', metadata, - Column('ID', BIGINT(unsigned=True), primary_key=True), - Column('ReqTypeID', ForeignKey('RequestTypes.ID', ondelete="NO ACTION"), nullable=False), - Column('PackageBaseID', ForeignKey('PackageBases.ID', ondelete='SET NULL')), - Column('PackageBaseName', String(255), nullable=False), - Column('MergeBaseName', String(255)), - Column('UsersID', ForeignKey('Users.ID', ondelete='SET NULL')), - Column('Comments', Text, nullable=False), - Column('ClosureComment', Text, nullable=False), - Column('RequestTS', BIGINT(unsigned=True), nullable=False, server_default=text("0")), - Column('ClosedTS', BIGINT(unsigned=True)), - Column('ClosedUID', ForeignKey('Users.ID', ondelete='SET NULL')), - Column('Status', TINYINT(unsigned=True), nullable=False, server_default=text("0")), - Index('RequestsPackageBaseID', 'PackageBaseID'), - Index('RequestsUsersID', 'UsersID'), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "PackageRequests", + metadata, + Column("ID", BIGINT(unsigned=True), primary_key=True), + Column( + "ReqTypeID", ForeignKey("RequestTypes.ID", ondelete="NO ACTION"), nullable=False + ), + Column("PackageBaseID", ForeignKey("PackageBases.ID", ondelete="SET NULL")), + Column("PackageBaseName", String(255), nullable=False), + Column("MergeBaseName", String(255)), + Column("UsersID", ForeignKey("Users.ID", ondelete="SET NULL")), + Column("Comments", Text, nullable=False), + Column("ClosureComment", Text, nullable=False), + Column( + "RequestTS", BIGINT(unsigned=True), nullable=False, server_default=text("0") + ), + Column("ClosedTS", BIGINT(unsigned=True)), + Column("ClosedUID", ForeignKey("Users.ID", ondelete="SET NULL")), + Column("Status", TINYINT(unsigned=True), nullable=False, server_default=text("0")), + Index("RequestsPackageBaseID", "PackageBaseID"), + Index("RequestsUsersID", "UsersID"), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Vote information -TU_VoteInfo = Table( - 'TU_VoteInfo', metadata, - Column('ID', INTEGER(unsigned=True), primary_key=True), - Column('Agenda', Text, nullable=False), - Column('User', String(32), nullable=False), - Column('Submitted', BIGINT(unsigned=True), nullable=False), - Column('End', BIGINT(unsigned=True), nullable=False), - Column('Quorum', - DECIMAL(2, 2, unsigned=True) - if db_backend == "mysql" else String(5), - nullable=False), - Column('SubmitterID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), - Column('Yes', INTEGER(unsigned=True), nullable=False, server_default=text("'0'")), - Column('No', INTEGER(unsigned=True), nullable=False, server_default=text("'0'")), - Column('Abstain', INTEGER(unsigned=True), nullable=False, server_default=text("'0'")), - Column('ActiveTUs', INTEGER(unsigned=True), nullable=False, server_default=text("'0'")), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', +VoteInfo = Table( + "VoteInfo", + metadata, + Column("ID", INTEGER(unsigned=True), primary_key=True), + Column("Agenda", Text, nullable=False), + Column("User", String(32), nullable=False), + Column("Submitted", BIGINT(unsigned=True), nullable=False), + Column("End", BIGINT(unsigned=True), nullable=False), + Column( + "Quorum", + DECIMAL(2, 2, unsigned=True) if db_backend == "mysql" else String(5), + nullable=False, + ), + Column("SubmitterID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), + Column("Yes", INTEGER(unsigned=True), nullable=False, server_default=text("'0'")), + Column("No", INTEGER(unsigned=True), nullable=False, server_default=text("'0'")), + Column( + "Abstain", INTEGER(unsigned=True), nullable=False, server_default=text("'0'") + ), + Column( + "ActiveUsers", + INTEGER(unsigned=True), + nullable=False, + server_default=text("'0'"), + ), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Individual vote records -TU_Votes = Table( - 'TU_Votes', metadata, - Column('VoteID', ForeignKey('TU_VoteInfo.ID', ondelete='CASCADE'), nullable=False), - Column('UserID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), - mysql_engine='InnoDB', +Votes = Table( + "Votes", + metadata, + Column("VoteID", ForeignKey("VoteInfo.ID", ondelete="CASCADE"), nullable=False), + Column("UserID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), + mysql_engine="InnoDB", ) # Malicious user banning Bans = Table( - 'Bans', metadata, - Column('IPAddress', String(45), primary_key=True), - Column('BanTS', TIMESTAMP, nullable=False), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "Bans", + metadata, + Column("IPAddress", String(45), primary_key=True), + Column("BanTS", TIMESTAMP, nullable=False), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Terms and Conditions Terms = Table( - 'Terms', metadata, - Column('ID', INTEGER(unsigned=True), primary_key=True), - Column('Description', String(255), nullable=False), - Column('URL', String(8000), nullable=False), - Column('Revision', INTEGER(unsigned=True), nullable=False, server_default=text("1")), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "Terms", + metadata, + Column("ID", INTEGER(unsigned=True), primary_key=True), + Column("Description", String(255), nullable=False), + Column("URL", String(8000), nullable=False), + Column( + "Revision", INTEGER(unsigned=True), nullable=False, server_default=text("1") + ), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) # Terms and Conditions accepted by users AcceptedTerms = Table( - 'AcceptedTerms', metadata, - Column('UsersID', ForeignKey('Users.ID', ondelete='CASCADE'), nullable=False), - Column('TermsID', ForeignKey('Terms.ID', ondelete='CASCADE'), nullable=False), - Column('Revision', INTEGER(unsigned=True), nullable=False, server_default=text("0")), - mysql_engine='InnoDB', + "AcceptedTerms", + metadata, + Column("UsersID", ForeignKey("Users.ID", ondelete="CASCADE"), nullable=False), + Column("TermsID", ForeignKey("Terms.ID", ondelete="CASCADE"), nullable=False), + Column( + "Revision", INTEGER(unsigned=True), nullable=False, server_default=text("0") + ), + mysql_engine="InnoDB", ) # Rate limits for API ApiRateLimit = Table( - 'ApiRateLimit', metadata, - Column('IP', String(45), primary_key=True, unique=True, default=str()), - Column('Requests', INTEGER(11), nullable=False), - Column('WindowStart', BIGINT(20), nullable=False), - Index('ApiRateLimitWindowStart', 'WindowStart'), - mysql_engine='InnoDB', - mysql_charset='utf8mb4', - mysql_collate='utf8mb4_general_ci', + "ApiRateLimit", + metadata, + Column("IP", String(45), primary_key=True, unique=True, default=str()), + Column("Requests", INTEGER(11), nullable=False), + Column("WindowStart", BIGINT(20), nullable=False), + Index("ApiRateLimitWindowStart", "WindowStart"), + mysql_engine="InnoDB", + mysql_charset="utf8mb4", + mysql_collate="utf8mb4_general_ci", ) diff --git a/aurweb/scripts/adduser.py b/aurweb/scripts/adduser.py index 4cc059d1..dc928b1f 100644 --- a/aurweb/scripts/adduser.py +++ b/aurweb/scripts/adduser.py @@ -6,12 +6,12 @@ See `aurweb-adduser --help` for documentation. Copyright (C) 2022 aurweb Development Team All Rights Reserved """ + import argparse import sys import traceback import aurweb.models.account_type as at - from aurweb import db from aurweb.models.account_type import AccountType from aurweb.models.ssh_pub_key import SSHPubKey, get_fingerprint @@ -30,8 +30,9 @@ def parse_args(): parser.add_argument("--ssh-pubkey", help="SSH PubKey") choices = at.ACCOUNT_TYPE_NAME.values() - parser.add_argument("-t", "--type", help="Account Type", - choices=choices, default=at.USER) + parser.add_argument( + "-t", "--type", help="Account Type", choices=choices, default=at.USER + ) return parser.parse_args() @@ -40,25 +41,29 @@ def main(): args = parse_args() db.get_engine() - type = db.query(AccountType, - AccountType.AccountType == args.type).first() + type = db.query(AccountType, AccountType.AccountType == args.type).first() with db.begin(): - user = db.create(User, Username=args.username, - Email=args.email, Passwd=args.password, - RealName=args.realname, IRCNick=args.ircnick, - PGPKey=args.pgp_key, AccountType=type) + user = db.create( + User, + Username=args.username, + Email=args.email, + Passwd=args.password, + RealName=args.realname, + IRCNick=args.ircnick, + PGPKey=args.pgp_key, + AccountType=type, + ) if args.ssh_pubkey: pubkey = args.ssh_pubkey.strip() # Remove host from the pubkey if it's there. - pubkey = ' '.join(pubkey.split(' ')[:2]) + pubkey = " ".join(pubkey.split(" ")[:2]) with db.begin(): - db.create(SSHPubKey, - User=user, - PubKey=pubkey, - Fingerprint=get_fingerprint(pubkey)) + db.create( + SSHPubKey, User=user, PubKey=pubkey, Fingerprint=get_fingerprint(pubkey) + ) print(user.json()) return 0 diff --git a/aurweb/scripts/aurblup.py b/aurweb/scripts/aurblup.py index 9c9059ec..10da1d98 100755 --- a/aurweb/scripts/aurblup.py +++ b/aurweb/scripts/aurblup.py @@ -3,11 +3,9 @@ import re import pyalpm - from sqlalchemy import and_ import aurweb.config - from aurweb import db, util from aurweb.models import OfficialProvider @@ -18,8 +16,8 @@ def _main(force: bool = False): repomap = dict() db_path = aurweb.config.get("aurblup", "db-path") - sync_dbs = aurweb.config.get('aurblup', 'sync-dbs').split(' ') - server = aurweb.config.get('aurblup', 'server') + sync_dbs = aurweb.config.get("aurblup", "sync-dbs").split(" ") + server = aurweb.config.get("aurblup", "server") h = pyalpm.Handle("/", db_path) for sync_db in sync_dbs: @@ -35,28 +33,46 @@ def _main(force: bool = False): providers.add((pkg.name, pkg.name)) repomap[(pkg.name, pkg.name)] = repo.name for provision in pkg.provides: - provisionname = re.sub(r'(<|=|>).*', '', provision) + provisionname = re.sub(r"(<|=|>).*", "", provision) providers.add((pkg.name, provisionname)) repomap[(pkg.name, provisionname)] = repo.name with db.begin(): old_providers = set( - db.query(OfficialProvider).with_entities( + db.query(OfficialProvider) + .with_entities( OfficialProvider.Name.label("Name"), - OfficialProvider.Provides.label("Provides") - ).distinct().order_by("Name").all() + OfficialProvider.Provides.label("Provides"), + ) + .distinct() + .order_by("Name") + .all() ) + # delete providers not existing in any of our alpm repos for name, provides in old_providers.difference(providers): - db.delete_all(db.query(OfficialProvider).filter( - and_(OfficialProvider.Name == name, - OfficialProvider.Provides == provides) - )) + db.delete_all( + db.query(OfficialProvider).filter( + and_( + OfficialProvider.Name == name, + OfficialProvider.Provides == provides, + ) + ) + ) + # add new providers that do not yet exist in our DB for name, provides in providers.difference(old_providers): repo = repomap.get((name, provides)) - db.create(OfficialProvider, Name=name, - Repo=repo, Provides=provides) + db.create(OfficialProvider, Name=name, Repo=repo, Provides=provides) + + # update providers where a pkg was moved from one repo to another + all_providers = db.query(OfficialProvider) + + for op in all_providers: + new_repo = repomap.get((op.Name, op.Provides)) + + if op.Repo != new_repo: + op.Repo = new_repo def main(force: bool = False): @@ -64,5 +80,5 @@ def main(force: bool = False): _main(force) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/aurweb/scripts/config.py b/aurweb/scripts/config.py index e7c91dd1..4da3296e 100644 --- a/aurweb/scripts/config.py +++ b/aurweb/scripts/config.py @@ -3,6 +3,7 @@ Perform an action on the aurweb config. When AUR_CONFIG_IMMUTABLE is set, the `set` action is noop. """ + import argparse import configparser import os @@ -50,12 +51,12 @@ def parse_args(): actions = ["get", "set", "unset"] parser = argparse.ArgumentParser( description="aurweb configuration tool", - formatter_class=lambda prog: fmt_cls(prog=prog, max_help_position=80)) + formatter_class=lambda prog: fmt_cls(prog=prog, max_help_position=80), + ) parser.add_argument("action", choices=actions, help="script action") parser.add_argument("section", help="config section") parser.add_argument("option", help="config option") - parser.add_argument("value", nargs="?", default=0, - help="config option value") + parser.add_argument("value", nargs="?", default=0, help="config option value") return parser.parse_args() diff --git a/aurweb/scripts/git_archive.py b/aurweb/scripts/git_archive.py new file mode 100644 index 00000000..8e47cb77 --- /dev/null +++ b/aurweb/scripts/git_archive.py @@ -0,0 +1,125 @@ +import argparse +import importlib +import os +import sys +import traceback +from datetime import UTC, datetime + +import orjson +import pygit2 + +from aurweb import config + +# Constants +REF = "refs/heads/master" +ORJSON_OPTS = orjson.OPT_SORT_KEYS | orjson.OPT_INDENT_2 + + +def init_repository(git_info) -> None: + pygit2.init_repository(git_info.path) + repo = pygit2.Repository(git_info.path) + for k, v in git_info.config.items(): + repo.config[k] = v + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--spec", + type=str, + required=True, + help="name of spec module in the aurweb.archives.spec package", + ) + return parser.parse_args() + + +def update_repository(repo: pygit2.Repository): + # Use git status to determine file changes + has_changes = False + changes = repo.status() + for filepath, flags in changes.items(): + if flags != pygit2.GIT_STATUS_CURRENT: + has_changes = True + break + + if has_changes: + print("diff detected, committing") + # Add everything in the tree. + print("adding files to git tree") + + # Add the tree to staging + repo.index.read() + repo.index.add_all() + repo.index.write() + tree = repo.index.write_tree() + + # Determine base commit; if repo.head.target raises GitError, + # we have no current commits + try: + base = [repo.head.target] + except pygit2.GitError: + base = [] + + utcnow = datetime.now(UTC) + author = pygit2.Signature( + config.get("git-archive", "author"), + config.get("git-archive", "author-email"), + int(utcnow.timestamp()), + 0, + ) + + # Commit the changes + timestamp = utcnow.strftime("%Y-%m-%d %H:%M:%S") + title = f"update - {timestamp}" + repo.create_commit(REF, author, author, title, tree, base) + + print("committed changes") + else: + print("no diff detected") + + +def main() -> int: + args = parse_args() + + print(f"loading '{args.spec}' spec") + spec_package = "aurweb.archives.spec" + module_path = f"{spec_package}.{args.spec}" + spec_module = importlib.import_module(module_path) + print(f"loaded '{args.spec}'") + + # Track repositories that the spec modifies. After we run + # through specs, we want to make a single commit for all + # repositories that contain changes. + repos = dict() + + print(f"running '{args.spec}' spec...") + spec = spec_module.Spec() + for output in spec.generate(): + if not os.path.exists(output.git_info.path / ".git"): + init_repository(output.git_info) + + path = output.git_info.path / output.filename + with open(path, "wb") as f: + f.write(output.data) + + if output.git_info.path not in repos: + repos[output.git_info.path] = pygit2.Repository(output.git_info.path) + + print(f"done running '{args.spec}' spec") + + print("processing repositories") + for path in spec.repos: + print(f"processing repository: {path}") + update_repository(pygit2.Repository(path)) + + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except KeyboardInterrupt: + sys.exit(0) + except Exception: + traceback.print_exc() + sys.exit(1) diff --git a/aurweb/scripts/mkpkglists.py b/aurweb/scripts/mkpkglists.py index 00096d74..d85a79b9 100755 --- a/aurweb/scripts/mkpkglists.py +++ b/aurweb/scripts/mkpkglists.py @@ -24,22 +24,18 @@ import io import os import shutil import sys -import tempfile - from collections import defaultdict -from typing import Any, Dict +from typing import Any import orjson - from sqlalchemy import literal, orm import aurweb.config - -from aurweb import db, filters, logging, models, util +from aurweb import aur_logging, db, filters, models, util from aurweb.benchmark import Benchmark from aurweb.models import Package, PackageBase, User -logger = logging.get_logger("aurweb.scripts.mkpkglists") +logger = aur_logging.get_logger("aurweb.scripts.mkpkglists") TYPE_MAP = { @@ -90,68 +86,83 @@ def get_extended_dict(query: orm.Query): def get_extended_fields(): subqueries = [ # PackageDependency - db.query( - models.PackageDependency - ).join(models.DependencyType).with_entities( + db.query(models.PackageDependency) + .join(models.DependencyType) + .with_entities( models.PackageDependency.PackageID.label("ID"), models.DependencyType.Name.label("Type"), models.PackageDependency.DepName.label("Name"), - models.PackageDependency.DepCondition.label("Cond") - ).distinct().order_by("Name"), - + models.PackageDependency.DepCondition.label("Cond"), + ) + .distinct() # A package could have the same dependency multiple times + .order_by("Name"), # PackageRelation - db.query( - models.PackageRelation - ).join(models.RelationType).with_entities( + db.query(models.PackageRelation) + .join(models.RelationType) + .with_entities( models.PackageRelation.PackageID.label("ID"), models.RelationType.Name.label("Type"), models.PackageRelation.RelName.label("Name"), - models.PackageRelation.RelCondition.label("Cond") - ).distinct().order_by("Name"), - + models.PackageRelation.RelCondition.label("Cond"), + ) + .distinct() # A package could have the same relation multiple times + .order_by("Name"), # Groups - db.query(models.PackageGroup).join( - models.Group, - models.PackageGroup.GroupID == models.Group.ID - ).with_entities( + db.query(models.PackageGroup) + .join(models.Group, models.PackageGroup.GroupID == models.Group.ID) + .with_entities( models.PackageGroup.PackageID.label("ID"), literal("Groups").label("Type"), models.Group.Name.label("Name"), - literal(str()).label("Cond") - ).distinct().order_by("Name"), - + literal(str()).label("Cond"), + ) + .order_by("Name"), # Licenses - db.query(models.PackageLicense).join( - models.License, - models.PackageLicense.LicenseID == models.License.ID - ).with_entities( + db.query(models.PackageLicense) + .join(models.License, models.PackageLicense.LicenseID == models.License.ID) + .with_entities( models.PackageLicense.PackageID.label("ID"), literal("License").label("Type"), models.License.Name.label("Name"), - literal(str()).label("Cond") - ).distinct().order_by("Name"), - + literal(str()).label("Cond"), + ) + .order_by("Name"), # Keywords - db.query(models.PackageKeyword).join( - models.Package, - Package.PackageBaseID == models.PackageKeyword.PackageBaseID - ).with_entities( + db.query(models.PackageKeyword) + .join( + models.Package, Package.PackageBaseID == models.PackageKeyword.PackageBaseID + ) + .with_entities( models.Package.ID.label("ID"), literal("Keywords").label("Type"), models.PackageKeyword.Keyword.label("Name"), - literal(str()).label("Cond") - ).distinct().order_by("Name") + literal(str()).label("Cond"), + ) + .order_by("Name"), + # Co-Maintainer + db.query(models.PackageComaintainer) + .join(models.User, models.User.ID == models.PackageComaintainer.UsersID) + .join( + models.Package, + models.Package.PackageBaseID == models.PackageComaintainer.PackageBaseID, + ) + .with_entities( + models.Package.ID, + literal("CoMaintainers").label("Type"), + models.User.Username.label("Name"), + literal(str()).label("Cond"), + ) + .distinct() # A package could have the same co-maintainer multiple times + .order_by("Name"), ] query = subqueries[0].union_all(*subqueries[1:]) return get_extended_dict(query) -EXTENDED_FIELD_HANDLERS = { - "--extended": get_extended_fields -} +EXTENDED_FIELD_HANDLERS = {"--extended": get_extended_fields} -def as_dict(package: Package) -> Dict[str, Any]: +def as_dict(package: Package) -> dict[str, Any]: return { "ID": package.ID, "Name": package.Name, @@ -164,6 +175,7 @@ def as_dict(package: Package) -> Dict[str, Any]: "Popularity": float(package.Popularity), "OutOfDate": package.OutOfDate, "Maintainer": package.Maintainer, + "Submitter": package.Submitter, "FirstSubmitted": package.FirstSubmitted, "LastModified": package.LastModified, } @@ -181,49 +193,54 @@ def _main(): archivedir = aurweb.config.get("mkpkglists", "archivedir") os.makedirs(archivedir, exist_ok=True) - PACKAGES = aurweb.config.get('mkpkglists', 'packagesfile') - META = aurweb.config.get('mkpkglists', 'packagesmetafile') - META_EXT = aurweb.config.get('mkpkglists', 'packagesmetaextfile') - PKGBASE = aurweb.config.get('mkpkglists', 'pkgbasefile') - USERS = aurweb.config.get('mkpkglists', 'userfile') + PACKAGES = aurweb.config.get("mkpkglists", "packagesfile") + META = aurweb.config.get("mkpkglists", "packagesmetafile") + META_EXT = aurweb.config.get("mkpkglists", "packagesmetaextfile") + PKGBASE = aurweb.config.get("mkpkglists", "pkgbasefile") + USERS = aurweb.config.get("mkpkglists", "userfile") bench = Benchmark() + logger.warning(f"{sys.argv[0]} is deprecated and will be soon be removed") logger.info("Started re-creating archives, wait a while...") - query = db.query(Package).join( - PackageBase, - PackageBase.ID == Package.PackageBaseID - ).join( - User, - PackageBase.MaintainerUID == User.ID, - isouter=True - ).filter(PackageBase.PackagerUID.isnot(None)).with_entities( - Package.ID, - Package.Name, - PackageBase.ID.label("PackageBaseID"), - PackageBase.Name.label("PackageBase"), - Package.Version, - Package.Description, - Package.URL, - PackageBase.NumVotes, - PackageBase.Popularity, - PackageBase.OutOfDateTS.label("OutOfDate"), - User.Username.label("Maintainer"), - PackageBase.SubmittedTS.label("FirstSubmitted"), - PackageBase.ModifiedTS.label("LastModified") - ).distinct().order_by("Name") + Submitter = orm.aliased(User) + + query = ( + db.query(Package) + .join(PackageBase, PackageBase.ID == Package.PackageBaseID) + .join(User, PackageBase.MaintainerUID == User.ID, isouter=True) + .join(Submitter, PackageBase.SubmitterUID == Submitter.ID, isouter=True) + .with_entities( + Package.ID, + Package.Name, + PackageBase.ID.label("PackageBaseID"), + PackageBase.Name.label("PackageBase"), + Package.Version, + Package.Description, + Package.URL, + PackageBase.NumVotes, + PackageBase.Popularity, + PackageBase.OutOfDateTS.label("OutOfDate"), + User.Username.label("Maintainer"), + Submitter.Username.label("Submitter"), + PackageBase.SubmittedTS.label("FirstSubmitted"), + PackageBase.ModifiedTS.label("LastModified"), + ) + .order_by("Name") + ) # Produce packages-meta-v1.json.gz output = list() snapshot_uri = aurweb.config.get("options", "snapshot_uri") - tmpdir = tempfile.mkdtemp() - tmp_packages = os.path.join(tmpdir, os.path.basename(PACKAGES)) - tmp_meta = os.path.join(tmpdir, os.path.basename(META)) - tmp_metaext = os.path.join(tmpdir, os.path.basename(META_EXT)) + tmp_packages = f"{PACKAGES}.tmp" + tmp_meta = f"{META}.tmp" + tmp_metaext = f"{META_EXT}.tmp" gzips = { - "packages": gzip.open(tmp_packages, "wt"), - "meta": gzip.open(tmp_meta, "wb"), + "packages": gzip.GzipFile( + filename=PACKAGES, mode="wb", fileobj=open(tmp_packages, "wb") + ), + "meta": gzip.GzipFile(filename=META, mode="wb", fileobj=open(tmp_meta, "wb")), } # Append list opening to the metafile. @@ -232,7 +249,9 @@ def _main(): # Produce packages.gz + packages-meta-ext-v1.json.gz extended = False if len(sys.argv) > 1 and sys.argv[1] in EXTENDED_FIELD_HANDLERS: - gzips["meta_ext"] = gzip.open(tmp_metaext, "wb") + gzips["meta_ext"] = gzip.GzipFile( + filename=META_EXT, mode="wb", fileobj=open(tmp_metaext, "wb") + ) # Append list opening to the meta_ext file. gzips.get("meta_ext").write(b"[\n") f = EXTENDED_FIELD_HANDLERS.get(sys.argv[1]) @@ -241,28 +260,29 @@ def _main(): results = query.all() n = len(results) - 1 - for i, result in enumerate(results): - # Append to packages.gz. - gzips.get("packages").write(f"{result.Name}\n") + with io.TextIOWrapper(gzips.get("packages")) as p: + for i, result in enumerate(results): + # Append to packages.gz. + p.write(f"{result.Name}\n") - # Construct our result JSON dictionary. - item = as_dict(result) - item["URLPath"] = snapshot_uri % result.Name + # Construct our result JSON dictionary. + item = as_dict(result) + item["URLPath"] = snapshot_uri % result.Name - # We stream out package json objects line per line, so - # we also need to include the ',' character at the end - # of package lines (excluding the last package). - suffix = b",\n" if i < n else b'\n' + # We stream out package json objects line per line, so + # we also need to include the ',' character at the end + # of package lines (excluding the last package). + suffix = b",\n" if i < n else b"\n" - # Write out to packagesmetafile - output.append(item) - gzips.get("meta").write(orjson.dumps(output[-1]) + suffix) + # Write out to packagesmetafile + output.append(item) + gzips.get("meta").write(orjson.dumps(output[-1]) + suffix) - if extended: - # Write out to packagesmetaextfile. - data_ = data.get(result.ID, {}) - output[-1].update(data_) - gzips.get("meta_ext").write(orjson.dumps(output[-1]) + suffix) + if extended: + # Write out to packagesmetaextfile. + data_ = data.get(result.ID, {}) + output[-1].update(data_) + gzips.get("meta_ext").write(orjson.dumps(output[-1]) + suffix) # Append the list closing to meta/meta_ext. gzips.get("meta").write(b"]") @@ -273,16 +293,19 @@ def _main(): util.apply_all(gzips.values(), lambda gz: gz.close()) # Produce pkgbase.gz - query = db.query(PackageBase.Name).filter( - PackageBase.PackagerUID.isnot(None)).all() - tmp_pkgbase = os.path.join(tmpdir, os.path.basename(PKGBASE)) - with gzip.open(tmp_pkgbase, "wt") as f: + query = db.query(PackageBase.Name).all() + tmp_pkgbase = f"{PKGBASE}.tmp" + pkgbase_gzip = gzip.GzipFile( + filename=PKGBASE, mode="wb", fileobj=open(tmp_pkgbase, "wb") + ) + with io.TextIOWrapper(pkgbase_gzip) as f: f.writelines([f"{base.Name}\n" for i, base in enumerate(query)]) # Produce users.gz query = db.query(User.Username).all() - tmp_users = os.path.join(tmpdir, os.path.basename(USERS)) - with gzip.open(tmp_users, "wt") as f: + tmp_users = f"{USERS}.tmp" + users_gzip = gzip.GzipFile(filename=USERS, mode="wb", fileobj=open(tmp_users, "wb")) + with io.TextIOWrapper(users_gzip) as f: f.writelines([f"{user.Username}\n" for i, user in enumerate(query)]) files = [ @@ -296,7 +319,7 @@ def _main(): for src, dst in files: checksum = sha256sum(src) - base = os.path.basename(src) + base = os.path.basename(dst) checksum_formatted = f"SHA256 ({base}) = {checksum}" checksum_file = f"{dst}.sha256" @@ -306,7 +329,6 @@ def _main(): # Move the new archive into its rightful place. shutil.move(src, dst) - os.removedirs(tmpdir) seconds = filters.number_format(bench.end(), 4) logger.info(f"Completed in {seconds} seconds.") @@ -317,5 +339,5 @@ def main(): _main() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/aurweb/scripts/notify.py b/aurweb/scripts/notify.py index 6afa65ae..0e548be4 100755 --- a/aurweb/scripts/notify.py +++ b/aurweb/scripts/notify.py @@ -13,27 +13,26 @@ import aurweb.config import aurweb.db import aurweb.filters import aurweb.l10n - -from aurweb import db, logging +from aurweb import aur_logging, db from aurweb.models import PackageBase, User from aurweb.models.package_comaintainer import PackageComaintainer from aurweb.models.package_comment import PackageComment from aurweb.models.package_notification import PackageNotification from aurweb.models.package_request import PackageRequest from aurweb.models.request_type import RequestType -from aurweb.models.tu_vote import TUVote +from aurweb.models.vote import Vote -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) -aur_location = aurweb.config.get('options', 'aur_location') +aur_location = aurweb.config.get("options", "aur_location") def headers_msgid(thread_id): - return {'Message-ID': thread_id} + return {"Message-ID": thread_id} def headers_reply(thread_id): - return {'In-Reply-To': thread_id, 'References': thread_id} + return {"In-Reply-To": thread_id, "References": thread_id} class Notification: @@ -46,68 +45,68 @@ class Notification: def get_cc(self): return [] + def get_bcc(self): + return [] + def get_body_fmt(self, lang): - body = '' + body = "" for line in self.get_body(lang).splitlines(): - if line == '--': - body += '--\n' + if line == "--": + body += "--\n" continue - body += textwrap.fill(line, break_long_words=False) + '\n' + body += textwrap.fill(line, break_long_words=False) + "\n" for i, ref in enumerate(self.get_refs()): - body += '\n' + '[%d] %s' % (i + 1, ref) + body += "\n" + "[%d] %s" % (i + 1, ref) return body.rstrip() def _send(self) -> None: - sendmail = aurweb.config.get('notifications', 'sendmail') - sender = aurweb.config.get('notifications', 'sender') - reply_to = aurweb.config.get('notifications', 'reply-to') + sendmail = aurweb.config.get("notifications", "sendmail") + sender = aurweb.config.get("notifications", "sender") + reply_to = aurweb.config.get("notifications", "reply-to") reason = self.__class__.__name__ - if reason.endswith('Notification'): - reason = reason[:-len('Notification')] + if reason.endswith("Notification"): + reason = reason[: -len("Notification")] for recipient in self.get_recipients(): to, lang = recipient - msg = email.mime.text.MIMEText(self.get_body_fmt(lang), - 'plain', 'utf-8') - msg['Subject'] = self.get_subject(lang) - msg['From'] = sender - msg['Reply-to'] = reply_to - msg['To'] = to + msg = email.mime.text.MIMEText(self.get_body_fmt(lang), "plain", "utf-8") + msg["Subject"] = self.get_subject(lang) + msg["From"] = sender + msg["Reply-to"] = reply_to + msg["To"] = to if self.get_cc(): - msg['Cc'] = str.join(', ', self.get_cc()) - msg['X-AUR-Reason'] = reason - msg['Date'] = email.utils.formatdate(localtime=True) + msg["Cc"] = str.join(", ", self.get_cc()) + msg["X-AUR-Reason"] = reason + msg["Date"] = email.utils.formatdate(localtime=True) for key, value in self.get_headers().items(): msg[key] = value - sendmail = aurweb.config.get('notifications', 'sendmail') + sendmail = aurweb.config.get("notifications", "sendmail") if sendmail: # send email using the sendmail binary specified in the # configuration file - p = subprocess.Popen([sendmail, '-t', '-oi'], - stdin=subprocess.PIPE) + p = subprocess.Popen([sendmail, "-t", "-oi"], stdin=subprocess.PIPE) p.communicate(msg.as_bytes()) else: # send email using smtplib; no local MTA required - server_addr = aurweb.config.get('notifications', 'smtp-server') - server_port = aurweb.config.getint('notifications', - 'smtp-port') - use_ssl = aurweb.config.getboolean('notifications', - 'smtp-use-ssl') - use_starttls = aurweb.config.getboolean('notifications', - 'smtp-use-starttls') - user = aurweb.config.get('notifications', 'smtp-user') - passwd = aurweb.config.get('notifications', 'smtp-password') + server_addr = aurweb.config.get("notifications", "smtp-server") + server_port = aurweb.config.getint("notifications", "smtp-port") + use_ssl = aurweb.config.getboolean("notifications", "smtp-use-ssl") + use_starttls = aurweb.config.getboolean( + "notifications", "smtp-use-starttls" + ) + user = aurweb.config.get("notifications", "smtp-user") + passwd = aurweb.config.get("notifications", "smtp-password") classes = { False: smtplib.SMTP, True: smtplib.SMTP_SSL, } - smtp_timeout = aurweb.config.getint("notifications", - "smtp-timeout") - server = classes[use_ssl](server_addr, server_port, - timeout=smtp_timeout) + smtp_timeout = aurweb.config.getint("notifications", "smtp-timeout") + server = classes[use_ssl]( + server_addr, server_port, timeout=smtp_timeout + ) if use_starttls: server.ehlo() @@ -118,7 +117,7 @@ class Notification: server.login(user, passwd) server.set_debuglevel(0) - deliver_to = [to] + self.get_cc() + deliver_to = [to] + self.get_cc() + self.get_bcc() server.sendmail(sender, deliver_to, msg.as_bytes()) server.quit() @@ -126,23 +125,28 @@ class Notification: try: self._send() except OSError as exc: - logger.error("Unable to emit notification due to an " - "OSError (precise exception following).") + logger.error( + "Unable to emit notification due to an " + "OSError (precise exception following)." + ) logger.error(str(exc)) class ResetKeyNotification(Notification): def __init__(self, uid): - - user = db.query(User).filter( - and_(User.ID == uid, User.Suspended == 0) - ).with_entities( - User.Username, - User.Email, - User.BackupEmail, - User.LangPreference, - User.ResetKey - ).order_by(User.Username.asc()).first() + user = ( + db.query(User) + .filter(and_(User.ID == uid, User.Suspended == 0)) + .with_entities( + User.Username, + User.Email, + User.BackupEmail, + User.LangPreference, + User.ResetKey, + ) + .order_by(User.Username.asc()) + .first() + ) self._username = user.Username self._to = user.Email @@ -159,55 +163,65 @@ class ResetKeyNotification(Notification): return [(self._to, self._lang)] def get_subject(self, lang): - return aurweb.l10n.translator.translate('AUR Password Reset', lang) + return aurweb.l10n.translator.translate("AUR Password Reset", lang) def get_body(self, lang): return aurweb.l10n.translator.translate( - 'A password reset request was submitted for the account ' - '{user} associated with your email address. If you wish to ' - 'reset your password follow the link [1] below, otherwise ' - 'ignore this message and nothing will happen.', - lang).format(user=self._username) + "A password reset request was submitted for the account " + "{user} associated with your email address. If you wish to " + "reset your password follow the link [1] below, otherwise " + "ignore this message and nothing will happen.", + lang, + ).format(user=self._username) def get_refs(self): - return (aur_location + '/passreset/?resetkey=' + self._resetkey,) + return (aur_location + "/passreset/?resetkey=" + self._resetkey,) class WelcomeNotification(ResetKeyNotification): def get_subject(self, lang): return aurweb.l10n.translator.translate( - 'Welcome to the Arch User Repository', - lang) + "Welcome to the Arch User Repository", lang + ) def get_body(self, lang): return aurweb.l10n.translator.translate( - 'Welcome to the Arch User Repository! In order to set an ' - 'initial password for your new account, please click the ' - 'link [1] below. If the link does not work, try copying and ' - 'pasting it into your browser.', lang) + "Welcome to the Arch User Repository! In order to set an " + "initial password for your new account, please click the " + "link [1] below. If the link does not work, try copying and " + "pasting it into your browser.", + lang, + ) class CommentNotification(Notification): def __init__(self, uid, pkgbase_id, comment_id): + self._user = db.query(User.Username).filter(User.ID == uid).first().Username + self._pkgbase = ( + db.query(PackageBase.Name).filter(PackageBase.ID == pkgbase_id).first().Name + ) - self._user = db.query(User.Username).filter( - User.ID == uid).first().Username - self._pkgbase = db.query(PackageBase.Name).filter( - PackageBase.ID == pkgbase_id).first().Name - - query = db.query(User).join(PackageNotification).filter( - and_(User.CommentNotify == 1, - PackageNotification.UserID != uid, - PackageNotification.PackageBaseID == pkgbase_id, - User.Suspended == 0) - ).with_entities( - User.Email, - User.LangPreference - ).distinct() + query = ( + db.query(User) + .join(PackageNotification) + .filter( + and_( + User.CommentNotify == 1, + PackageNotification.UserID != uid, + PackageNotification.PackageBaseID == pkgbase_id, + User.Suspended == 0, + ) + ) + .with_entities(User.Email, User.LangPreference) + .distinct() + ) self._recipients = [(u.Email, u.LangPreference) for u in query] - pkgcomment = db.query(PackageComment.Comments).filter( - PackageComment.ID == comment_id).first() + pkgcomment = ( + db.query(PackageComment.Comments) + .filter(PackageComment.ID == comment_id) + .first() + ) self._text = pkgcomment.Comments super().__init__() @@ -217,49 +231,55 @@ class CommentNotification(Notification): def get_subject(self, lang): return aurweb.l10n.translator.translate( - 'AUR Comment for {pkgbase}', - lang).format(pkgbase=self._pkgbase) + "AUR Comment for {pkgbase}", lang + ).format(pkgbase=self._pkgbase) def get_body(self, lang): body = aurweb.l10n.translator.translate( - '{user} [1] added the following comment to {pkgbase} [2]:', - lang).format(user=self._user, pkgbase=self._pkgbase) - body += '\n\n' + self._text + '\n\n--\n' - dnlabel = aurweb.l10n.translator.translate( - 'Disable notifications', lang) + "{user} [1] added the following comment to {pkgbase} [2]:", lang + ).format(user=self._user, pkgbase=self._pkgbase) + body += "\n\n" + self._text + "\n\n--\n" + dnlabel = aurweb.l10n.translator.translate("Disable notifications", lang) body += aurweb.l10n.translator.translate( - 'If you no longer wish to receive notifications about this ' - 'package, please go to the package page [2] and select ' - '"{label}".', lang).format(label=dnlabel) + "If you no longer wish to receive notifications about this " + "package, please go to the package page [2] and select " + '"{label}".', + lang, + ).format(label=dnlabel) return body def get_refs(self): - return (aur_location + '/account/' + self._user + '/', - aur_location + '/pkgbase/' + self._pkgbase + '/') + return ( + aur_location + "/account/" + self._user + "/", + aur_location + "/pkgbase/" + self._pkgbase + "/", + ) def get_headers(self): - thread_id = '' + thread_id = "" return headers_reply(thread_id) class UpdateNotification(Notification): def __init__(self, uid, pkgbase_id): + self._user = db.query(User.Username).filter(User.ID == uid).first().Username + self._pkgbase = ( + db.query(PackageBase.Name).filter(PackageBase.ID == pkgbase_id).first().Name + ) - self._user = db.query(User.Username).filter( - User.ID == uid).first().Username - self._pkgbase = db.query(PackageBase.Name).filter( - PackageBase.ID == pkgbase_id).first().Name - - query = db.query(User).join(PackageNotification).filter( - and_(User.UpdateNotify == 1, - PackageNotification.UserID != uid, - PackageNotification.PackageBaseID == pkgbase_id, - User.Suspended == 0) - ).with_entities( - User.Email, - User.LangPreference - ).distinct() + query = ( + db.query(User) + .join(PackageNotification) + .filter( + and_( + User.UpdateNotify == 1, + PackageNotification.UserID != uid, + PackageNotification.PackageBaseID == pkgbase_id, + User.Suspended == 0, + ) + ) + .with_entities(User.Email, User.LangPreference) + .distinct() + ) self._recipients = [(u.Email, u.LangPreference) for u in query] super().__init__() @@ -269,55 +289,63 @@ class UpdateNotification(Notification): def get_subject(self, lang): return aurweb.l10n.translator.translate( - 'AUR Package Update: {pkgbase}', - lang).format(pkgbase=self._pkgbase) + "AUR Package Update: {pkgbase}", lang + ).format(pkgbase=self._pkgbase) def get_body(self, lang): body = aurweb.l10n.translator.translate( - '{user} [1] pushed a new commit to {pkgbase} [2].', - lang).format(user=self._user, pkgbase=self._pkgbase) - body += '\n\n--\n' - dnlabel = aurweb.l10n.translator.translate( - 'Disable notifications', lang) + "{user} [1] pushed a new commit to {pkgbase} [2].", lang + ).format(user=self._user, pkgbase=self._pkgbase) + body += "\n\n--\n" + dnlabel = aurweb.l10n.translator.translate("Disable notifications", lang) body += aurweb.l10n.translator.translate( - 'If you no longer wish to receive notifications about this ' - 'package, please go to the package page [2] and select ' - '"{label}".', lang).format(label=dnlabel) + "If you no longer wish to receive notifications about this " + "package, please go to the package page [2] and select " + '"{label}".', + lang, + ).format(label=dnlabel) return body def get_refs(self): - return (aur_location + '/account/' + self._user + '/', - aur_location + '/pkgbase/' + self._pkgbase + '/') + return ( + aur_location + "/account/" + self._user + "/", + aur_location + "/pkgbase/" + self._pkgbase + "/", + ) def get_headers(self): - thread_id = '' + thread_id = "" return headers_reply(thread_id) class FlagNotification(Notification): def __init__(self, uid, pkgbase_id): + self._user = db.query(User.Username).filter(User.ID == uid).first().Username + self._pkgbase = ( + db.query(PackageBase.Name).filter(PackageBase.ID == pkgbase_id).first().Name + ) - self._user = db.query(User.Username).filter( - User.ID == uid).first().Username - self._pkgbase = db.query(PackageBase.Name).filter( - PackageBase.ID == pkgbase_id).first().Name - - query = db.query(User).join(PackageComaintainer, isouter=True).join( - PackageBase, - or_(PackageBase.MaintainerUID == User.ID, - PackageBase.ID == PackageComaintainer.PackageBaseID) - ).filter( - and_(PackageBase.ID == pkgbase_id, - User.Suspended == 0) - ).with_entities( - User.Email, - User.LangPreference - ).distinct() + query = ( + db.query(User) + .join(PackageComaintainer, isouter=True) + .join( + PackageBase, + or_( + PackageBase.MaintainerUID == User.ID, + PackageBase.ID == PackageComaintainer.PackageBaseID, + ), + ) + .filter(and_(PackageBase.ID == pkgbase_id, User.Suspended == 0)) + .with_entities(User.Email, User.LangPreference) + .distinct() + .order_by(User.Email) + ) self._recipients = [(u.Email, u.LangPreference) for u in query] - pkgbase = db.query(PackageBase.FlaggerComment).filter( - PackageBase.ID == pkgbase_id).first() + pkgbase = ( + db.query(PackageBase.FlaggerComment) + .filter(PackageBase.ID == pkgbase_id) + .first() + ) self._text = pkgbase.FlaggerComment super().__init__() @@ -327,43 +355,52 @@ class FlagNotification(Notification): def get_subject(self, lang): return aurweb.l10n.translator.translate( - 'AUR Out-of-date Notification for {pkgbase}', - lang).format(pkgbase=self._pkgbase) + "AUR Out-of-date Notification for {pkgbase}", lang + ).format(pkgbase=self._pkgbase) def get_body(self, lang): body = aurweb.l10n.translator.translate( - 'Your package {pkgbase} [1] has been flagged out-of-date by ' - '{user} [2]:', lang).format(pkgbase=self._pkgbase, - user=self._user) - body += '\n\n' + self._text + "Your package {pkgbase} [1] has been flagged out-of-date by " "{user} [2]:", + lang, + ).format(pkgbase=self._pkgbase, user=self._user) + body += "\n\n" + self._text return body def get_refs(self): - return (aur_location + '/pkgbase/' + self._pkgbase + '/', - aur_location + '/account/' + self._user + '/') + return ( + aur_location + "/pkgbase/" + self._pkgbase + "/", + aur_location + "/account/" + self._user + "/", + ) class OwnershipEventNotification(Notification): def __init__(self, uid, pkgbase_id): + self._user = db.query(User.Username).filter(User.ID == uid).first().Username + self._pkgbase = ( + db.query(PackageBase.Name).filter(PackageBase.ID == pkgbase_id).first().Name + ) - self._user = db.query(User.Username).filter( - User.ID == uid).first().Username - self._pkgbase = db.query(PackageBase.Name).filter( - PackageBase.ID == pkgbase_id).first().Name - - query = db.query(User).join(PackageNotification).filter( - and_(User.OwnershipNotify == 1, - PackageNotification.UserID != uid, - PackageNotification.PackageBaseID == pkgbase_id, - User.Suspended == 0) - ).with_entities( - User.Email, - User.LangPreference - ).distinct() + query = ( + db.query(User) + .join(PackageNotification) + .filter( + and_( + User.OwnershipNotify == 1, + PackageNotification.UserID != uid, + PackageNotification.PackageBaseID == pkgbase_id, + User.Suspended == 0, + ) + ) + .with_entities(User.Email, User.LangPreference) + .distinct() + ) self._recipients = [(u.Email, u.LangPreference) for u in query] - pkgbase = db.query(PackageBase.FlaggerComment).filter( - PackageBase.ID == pkgbase_id).first() + pkgbase = ( + db.query(PackageBase.FlaggerComment) + .filter(PackageBase.ID == pkgbase_id) + .first() + ) self._text = pkgbase.FlaggerComment super().__init__() @@ -373,39 +410,42 @@ class OwnershipEventNotification(Notification): def get_subject(self, lang): return aurweb.l10n.translator.translate( - 'AUR Ownership Notification for {pkgbase}', - lang).format(pkgbase=self._pkgbase) + "AUR Ownership Notification for {pkgbase}", lang + ).format(pkgbase=self._pkgbase) def get_refs(self): - return (aur_location + '/pkgbase/' + self._pkgbase + '/', - aur_location + '/account/' + self._user + '/') + return ( + aur_location + "/pkgbase/" + self._pkgbase + "/", + aur_location + "/account/" + self._user + "/", + ) class AdoptNotification(OwnershipEventNotification): def get_body(self, lang): return aurweb.l10n.translator.translate( - 'The package {pkgbase} [1] was adopted by {user} [2].', - lang).format(pkgbase=self._pkgbase, user=self._user) + "The package {pkgbase} [1] was adopted by {user} [2].", lang + ).format(pkgbase=self._pkgbase, user=self._user) class DisownNotification(OwnershipEventNotification): def get_body(self, lang): return aurweb.l10n.translator.translate( - 'The package {pkgbase} [1] was disowned by {user} ' - '[2].', lang).format(pkgbase=self._pkgbase, - user=self._user) + "The package {pkgbase} [1] was disowned by {user} " "[2].", lang + ).format(pkgbase=self._pkgbase, user=self._user) class ComaintainershipEventNotification(Notification): def __init__(self, uid, pkgbase_id): + self._pkgbase = ( + db.query(PackageBase.Name).filter(PackageBase.ID == pkgbase_id).first().Name + ) - self._pkgbase = db.query(PackageBase.Name).filter( - PackageBase.ID == pkgbase_id).first().Name - - user = db.query(User).filter(User.ID == uid).with_entities( - User.Email, - User.LangPreference - ).first() + user = ( + db.query(User) + .filter(User.ID == uid) + .with_entities(User.Email, User.LangPreference) + .first() + ) self._to = user.Email self._lang = user.LangPreference @@ -417,247 +457,58 @@ class ComaintainershipEventNotification(Notification): def get_subject(self, lang): return aurweb.l10n.translator.translate( - 'AUR Co-Maintainer Notification for {pkgbase}', - lang).format(pkgbase=self._pkgbase) + "AUR Co-Maintainer Notification for {pkgbase}", lang + ).format(pkgbase=self._pkgbase) def get_refs(self): - return (aur_location + '/pkgbase/' + self._pkgbase + '/',) + return (aur_location + "/pkgbase/" + self._pkgbase + "/",) class ComaintainerAddNotification(ComaintainershipEventNotification): def get_body(self, lang): return aurweb.l10n.translator.translate( - 'You were added to the co-maintainer list of {pkgbase} [1].', - lang).format(pkgbase=self._pkgbase) + "You were added to the co-maintainer list of {pkgbase} [1].", lang + ).format(pkgbase=self._pkgbase) class ComaintainerRemoveNotification(ComaintainershipEventNotification): def get_body(self, lang): return aurweb.l10n.translator.translate( - 'You were removed from the co-maintainer list of {pkgbase} ' - '[1].', lang).format(pkgbase=self._pkgbase) + "You were removed from the co-maintainer list of {pkgbase} " "[1].", lang + ).format(pkgbase=self._pkgbase) class DeleteNotification(Notification): def __init__(self, uid, old_pkgbase_id, new_pkgbase_id=None): - - self._user = db.query(User.Username).filter( - User.ID == uid).first().Username - self._old_pkgbase = db.query(PackageBase.Name).filter( - PackageBase.ID == old_pkgbase_id).first().Name + self._user = db.query(User.Username).filter(User.ID == uid).first().Username + self._old_pkgbase = ( + db.query(PackageBase.Name) + .filter(PackageBase.ID == old_pkgbase_id) + .first() + .Name + ) self._new_pkgbase = None if new_pkgbase_id: - self._new_pkgbase = db.query(PackageBase.Name).filter( - PackageBase.ID == new_pkgbase_id).first().Name + self._new_pkgbase = ( + db.query(PackageBase.Name) + .filter(PackageBase.ID == new_pkgbase_id) + .first() + .Name + ) - query = db.query(User).join(PackageNotification).filter( - and_(PackageNotification.UserID != uid, - PackageNotification.PackageBaseID == old_pkgbase_id, - User.Suspended == 0) - ).with_entities( - User.Email, - User.LangPreference - ).distinct() - self._recipients = [(u.Email, u.LangPreference) for u in query] - - super().__init__() - - def get_recipients(self): - return self._recipients - - def get_subject(self, lang): - return aurweb.l10n.translator.translate( - 'AUR Package deleted: {pkgbase}', - lang).format(pkgbase=self._old_pkgbase) - - def get_body(self, lang): - if self._new_pkgbase: - dnlabel = aurweb.l10n.translator.translate( - 'Disable notifications', lang) - return aurweb.l10n.translator.translate( - '{user} [1] merged {old} [2] into {new} [3].\n\n' - '--\n' - 'If you no longer wish receive notifications about the ' - 'new package, please go to [3] and click "{label}".', - lang).format(user=self._user, old=self._old_pkgbase, - new=self._new_pkgbase, label=dnlabel) - else: - return aurweb.l10n.translator.translate( - '{user} [1] deleted {pkgbase} [2].\n\n' - 'You will no longer receive notifications about this ' - 'package.', lang).format(user=self._user, - pkgbase=self._old_pkgbase) - - def get_refs(self): - refs = (aur_location + '/account/' + self._user + '/', - aur_location + '/pkgbase/' + self._old_pkgbase + '/') - if self._new_pkgbase: - refs += (aur_location + '/pkgbase/' + self._new_pkgbase + '/',) - return refs - - -class RequestOpenNotification(Notification): - def __init__(self, uid, reqid, reqtype, pkgbase_id, merge_into=None): - - self._user = db.query(User.Username).filter( - User.ID == uid).first().Username - self._pkgbase = db.query(PackageBase.Name).filter( - PackageBase.ID == pkgbase_id).first().Name - - self._to = aurweb.config.get('options', 'aur_request_ml') - - query = db.query(PackageRequest).join(PackageBase).join( - PackageComaintainer, - PackageComaintainer.PackageBaseID == PackageRequest.PackageBaseID, - isouter=True - ).join( - User, - or_(User.ID == PackageRequest.UsersID, - User.ID == PackageBase.MaintainerUID, - User.ID == PackageComaintainer.UsersID) - ).filter( - and_(PackageRequest.ID == reqid, - User.Suspended == 0) - ).with_entities( - User.Email - ).distinct() - self._cc = [u.Email for u in query] - - pkgreq = db.query(PackageRequest.Comments).filter( - PackageRequest.ID == reqid).first() - - self._text = pkgreq.Comments - self._reqid = int(reqid) - self._reqtype = reqtype - self._merge_into = merge_into - - def get_recipients(self): - return [(self._to, 'en')] - - def get_cc(self): - return self._cc - - def get_subject(self, lang): - return '[PRQ#%d] %s Request for %s' % \ - (self._reqid, self._reqtype.title(), self._pkgbase) - - def get_body(self, lang): - if self._merge_into: - body = '%s [1] filed a request to merge %s [2] into %s [3]:' % \ - (self._user, self._pkgbase, self._merge_into) - body += '\n\n' + self._text - else: - an = 'an' if self._reqtype[0] in 'aeiou' else 'a' - body = '%s [1] filed %s %s request for %s [2]:' % \ - (self._user, an, self._reqtype, self._pkgbase) - body += '\n\n' + self._text - return body - - def get_refs(self): - refs = (aur_location + '/account/' + self._user + '/', - aur_location + '/pkgbase/' + self._pkgbase + '/') - if self._merge_into: - refs += (aur_location + '/pkgbase/' + self._merge_into + '/',) - return refs - - def get_headers(self): - thread_id = '' - # Use a deterministic Message-ID for the first email referencing a - # request. - headers = headers_msgid(thread_id) - return headers - - -class RequestCloseNotification(Notification): - - def __init__(self, uid, reqid, reason): - user = db.query(User.Username).filter(User.ID == uid).first() - self._user = user.Username if user else None - - self._to = aurweb.config.get('options', 'aur_request_ml') - - query = db.query(PackageRequest).join(PackageBase).join( - PackageComaintainer, - PackageComaintainer.PackageBaseID == PackageRequest.PackageBaseID, - isouter=True - ).join( - User, - or_(User.ID == PackageRequest.UsersID, - User.ID == PackageBase.MaintainerUID, - User.ID == PackageComaintainer.UsersID) - ).filter( - and_(PackageRequest.ID == reqid, - User.Suspended == 0) - ).with_entities( - User.Email - ).distinct() - self._cc = [u.Email for u in query] - - pkgreq = db.query(PackageRequest).join(RequestType).filter( - PackageRequest.ID == reqid - ).with_entities( - PackageRequest.ClosureComment, - RequestType.Name, - PackageRequest.PackageBaseName - ).first() - - self._text = pkgreq.ClosureComment - self._reqtype = pkgreq.Name - self._pkgbase = pkgreq.PackageBaseName - - self._reqid = int(reqid) - self._reason = reason - - def get_recipients(self): - return [(self._to, 'en')] - - def get_cc(self): - return self._cc - - def get_subject(self, lang): - return '[PRQ#%d] %s Request for %s %s' % (self._reqid, - self._reqtype.title(), - self._pkgbase, - self._reason.title()) - - def get_body(self, lang): - if self._user: - body = 'Request #%d has been %s by %s [1]' % \ - (self._reqid, self._reason, self._user) - else: - body = 'Request #%d has been %s automatically by the Arch User ' \ - 'Repository package request system' % \ - (self._reqid, self._reason) - if self._text.strip() == '': - body += '.' - else: - body += ':\n\n' + self._text - return body - - def get_refs(self): - if self._user: - return (aur_location + '/account/' + self._user + '/',) - else: - return () - - def get_headers(self): - thread_id = '' - headers = headers_reply(thread_id) - return headers - - -class TUVoteReminderNotification(Notification): - def __init__(self, vote_id): - self._vote_id = int(vote_id) - - subquery = db.query(TUVote.UserID).filter(TUVote.VoteID == vote_id) - query = db.query(User).filter( - and_(User.AccountTypeID.in_((2, 4)), - ~User.ID.in_(subquery), - User.Suspended == 0) - ).with_entities( - User.Email, User.LangPreference + query = ( + db.query(User) + .join(PackageNotification) + .filter( + and_( + PackageNotification.UserID != uid, + PackageNotification.PackageBaseID == old_pkgbase_id, + User.Suspended == 0, + ) + ) + .with_entities(User.Email, User.LangPreference) + .distinct() ) self._recipients = [(u.Email, u.LangPreference) for u in query] @@ -668,36 +519,287 @@ class TUVoteReminderNotification(Notification): def get_subject(self, lang): return aurweb.l10n.translator.translate( - 'TU Vote Reminder: Proposal {id}', - lang).format(id=self._vote_id) + "AUR Package deleted: {pkgbase}", lang + ).format(pkgbase=self._old_pkgbase) + + def get_body(self, lang): + if self._new_pkgbase: + dnlabel = aurweb.l10n.translator.translate("Disable notifications", lang) + return aurweb.l10n.translator.translate( + "{user} [1] merged {old} [2] into {new} [3].\n\n" + "--\n" + "If you no longer wish receive notifications about the " + 'new package, please go to [3] and click "{label}".', + lang, + ).format( + user=self._user, + old=self._old_pkgbase, + new=self._new_pkgbase, + label=dnlabel, + ) + else: + return aurweb.l10n.translator.translate( + "{user} [1] deleted {pkgbase} [2].\n\n" + "You will no longer receive notifications about this " + "package.", + lang, + ).format(user=self._user, pkgbase=self._old_pkgbase) + + def get_refs(self): + refs = ( + aur_location + "/account/" + self._user + "/", + aur_location + "/pkgbase/" + self._old_pkgbase + "/", + ) + if self._new_pkgbase: + refs += (aur_location + "/pkgbase/" + self._new_pkgbase + "/",) + return refs + + +class RequestOpenNotification(Notification): + def __init__(self, uid, reqid, reqtype, pkgbase_id, merge_into=None): + self._user = db.query(User.Username).filter(User.ID == uid).first().Username + self._pkgbase = ( + db.query(PackageBase.Name).filter(PackageBase.ID == pkgbase_id).first().Name + ) + + self._to = aurweb.config.get("options", "aur_request_ml") + + query = ( + db.query(PackageRequest) + .join(PackageBase) + .join( + PackageComaintainer, + PackageComaintainer.PackageBaseID == PackageRequest.PackageBaseID, + isouter=True, + ) + .join( + User, + or_( + User.ID == PackageRequest.UsersID, + User.ID == PackageBase.MaintainerUID, + User.ID == PackageComaintainer.UsersID, + ), + ) + .filter(and_(PackageRequest.ID == reqid, User.Suspended == 0)) + .with_entities(User.Email, User.HideEmail) + .distinct() + ) + self._cc = [u.Email for u in query if u.HideEmail == 0] + self._bcc = [u.Email for u in query if u.HideEmail == 1] + + pkgreq = ( + db.query(PackageRequest.Comments).filter(PackageRequest.ID == reqid).first() + ) + + self._text = pkgreq.Comments + self._reqid = int(reqid) + self._reqtype = reqtype + self._merge_into = merge_into + + def get_recipients(self): + return [(self._to, "en")] + + def get_cc(self): + return self._cc + + def get_bcc(self): + return self._bcc + + def get_subject(self, lang): + return "[PRQ#%d] %s Request for %s" % ( + self._reqid, + self._reqtype.title(), + self._pkgbase, + ) + + def get_body(self, lang): + if self._merge_into: + body = "%s [1] filed a request to merge %s [2] into %s [3]:" % ( + self._user, + self._pkgbase, + self._merge_into, + ) + body += "\n\n" + self._text + else: + an = "an" if self._reqtype[0] in "aeiou" else "a" + body = "%s [1] filed %s %s request for %s [2]:" % ( + self._user, + an, + self._reqtype, + self._pkgbase, + ) + body += "\n\n" + self._text + return body + + def get_refs(self): + refs = ( + aur_location + "/account/" + self._user + "/", + aur_location + "/pkgbase/" + self._pkgbase + "/", + ) + if self._merge_into: + refs += (aur_location + "/pkgbase/" + self._merge_into + "/",) + return refs + + def get_headers(self): + thread_id = "" + # Use a deterministic Message-ID for the first email referencing a + # request. + headers = headers_msgid(thread_id) + return headers + + +class RequestCloseNotification(Notification): + def __init__(self, uid, reqid, reason): + user = db.query(User.Username).filter(User.ID == uid).first() + self._user = user.Username if user else None + + self._to = aurweb.config.get("options", "aur_request_ml") + + query = ( + db.query(PackageRequest) + .join(PackageBase) + .join( + PackageComaintainer, + PackageComaintainer.PackageBaseID == PackageRequest.PackageBaseID, + isouter=True, + ) + .join( + User, + or_( + User.ID == PackageRequest.UsersID, + User.ID == PackageBase.MaintainerUID, + User.ID == PackageComaintainer.UsersID, + ), + ) + .filter(and_(PackageRequest.ID == reqid, User.Suspended == 0)) + .with_entities(User.Email, User.HideEmail) + .distinct() + ) + self._cc = [u.Email for u in query if u.HideEmail == 0] + self._bcc = [u.Email for u in query if u.HideEmail == 1] + + pkgreq = ( + db.query(PackageRequest) + .join(RequestType) + .filter(PackageRequest.ID == reqid) + .with_entities( + PackageRequest.ClosureComment, + RequestType.Name, + PackageRequest.PackageBaseName, + ) + .first() + ) + + self._text = pkgreq.ClosureComment + self._reqtype = pkgreq.Name + self._pkgbase = pkgreq.PackageBaseName + + self._reqid = int(reqid) + self._reason = reason + + def get_recipients(self): + return [(self._to, "en")] + + def get_cc(self): + return self._cc + + def get_bcc(self): + return self._bcc + + def get_subject(self, lang): + return "[PRQ#%d] %s Request for %s %s" % ( + self._reqid, + self._reqtype.title(), + self._pkgbase, + self._reason.title(), + ) + + def get_body(self, lang): + if self._user: + body = "Request #%d has been %s by %s [1]" % ( + self._reqid, + self._reason, + self._user, + ) + else: + body = ( + "Request #%d has been %s automatically by the Arch User " + "Repository package request system" % (self._reqid, self._reason) + ) + if self._text.strip() == "": + body += "." + else: + body += ":\n\n" + self._text + return body + + def get_refs(self): + if self._user: + return (aur_location + "/account/" + self._user + "/",) + else: + return () + + def get_headers(self): + thread_id = "" + headers = headers_reply(thread_id) + return headers + + +class VoteReminderNotification(Notification): + def __init__(self, vote_id): + self._vote_id = int(vote_id) + + subquery = db.query(Vote.UserID).filter(Vote.VoteID == vote_id) + query = ( + db.query(User) + .filter( + and_( + User.AccountTypeID.in_((2, 4)), + ~User.ID.in_(subquery), + User.Suspended == 0, + ) + ) + .with_entities(User.Email, User.LangPreference) + ) + self._recipients = [(u.Email, u.LangPreference) for u in query] + + super().__init__() + + def get_recipients(self): + return self._recipients + + def get_subject(self, lang): + return aurweb.l10n.translator.translate( + "Package Maintainer Vote Reminder: Proposal {id}", lang + ).format(id=self._vote_id) def get_body(self, lang): return aurweb.l10n.translator.translate( - 'Please remember to cast your vote on proposal {id} [1]. ' - 'The voting period ends in less than 48 hours.', - lang).format(id=self._vote_id) + "Please remember to cast your vote on proposal {id} [1]. " + "The voting period ends in less than 48 hours.", + lang, + ).format(id=self._vote_id) def get_refs(self): - return (aur_location + '/tu/?id=' + str(self._vote_id),) + return (aur_location + "/package-maintainer/?id=" + str(self._vote_id),) def main(): db.get_engine() action = sys.argv[1] action_map = { - 'send-resetkey': ResetKeyNotification, - 'welcome': WelcomeNotification, - 'comment': CommentNotification, - 'update': UpdateNotification, - 'flag': FlagNotification, - 'adopt': AdoptNotification, - 'disown': DisownNotification, - 'comaintainer-add': ComaintainerAddNotification, - 'comaintainer-remove': ComaintainerRemoveNotification, - 'delete': DeleteNotification, - 'request-open': RequestOpenNotification, - 'request-close': RequestCloseNotification, - 'tu-vote-reminder': TUVoteReminderNotification, + "send-resetkey": ResetKeyNotification, + "welcome": WelcomeNotification, + "comment": CommentNotification, + "update": UpdateNotification, + "flag": FlagNotification, + "adopt": AdoptNotification, + "disown": DisownNotification, + "comaintainer-add": ComaintainerAddNotification, + "comaintainer-remove": ComaintainerRemoveNotification, + "delete": DeleteNotification, + "request-open": RequestOpenNotification, + "request-close": RequestCloseNotification, + "vote-reminder": VoteReminderNotification, } with db.begin(): @@ -705,5 +807,5 @@ def main(): notification.send() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/aurweb/scripts/pkgmaint.py b/aurweb/scripts/pkgmaint.py index 2a2c638a..3d695f8f 100755 --- a/aurweb/scripts/pkgmaint.py +++ b/aurweb/scripts/pkgmaint.py @@ -11,16 +11,22 @@ def _main(): limit_to = time.utcnow() - 86400 query = db.query(PackageBase).filter( - and_(PackageBase.SubmittedTS < limit_to, - PackageBase.PackagerUID.is_(None))) + and_(PackageBase.SubmittedTS < limit_to, PackageBase.PackagerUID.is_(None)) + ) db.delete_all(query) def main(): + # Previously used to clean up "reserved" packages which never got pushed. + # Let's deactivate this for now since "setup-repo" is gone and we see + # other issue where deletion of a user account might cause unintended + # removal of a package (where PackagerUID account was deleted) + return + db.get_engine() with db.begin(): _main() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/aurweb/scripts/popupdate.py b/aurweb/scripts/popupdate.py index a2a796fd..83506e22 100755 --- a/aurweb/scripts/popupdate.py +++ b/aurweb/scripts/popupdate.py @@ -1,16 +1,14 @@ #!/usr/bin/env python3 - -from typing import List +from datetime import datetime from sqlalchemy import and_, func -from sqlalchemy.sql.functions import coalesce -from sqlalchemy.sql.functions import sum as _sum +from sqlalchemy.sql.functions import coalesce, sum as _sum -from aurweb import db, time +from aurweb import config, db, time from aurweb.models import PackageBase, PackageVote -def run_variable(pkgbases: List[PackageBase] = []) -> None: +def run_variable(pkgbases: list[PackageBase] = []) -> None: """ Update popularity on a list of PackageBases. @@ -22,18 +20,26 @@ def run_variable(pkgbases: List[PackageBase] = []) -> None: now = time.utcnow() # NumVotes subquery. - votes_subq = db.get_session().query( - func.count("*") - ).select_from(PackageVote).filter( - PackageVote.PackageBaseID == PackageBase.ID + votes_subq = ( + db.get_session() + .query(func.count("*")) + .select_from(PackageVote) + .filter(PackageVote.PackageBaseID == PackageBase.ID) ) # Popularity subquery. - pop_subq = db.get_session().query( - coalesce(_sum(func.pow(0.98, (now - PackageVote.VoteTS) / 86400)), 0.0), - ).select_from(PackageVote).filter( - and_(PackageVote.PackageBaseID == PackageBase.ID, - PackageVote.VoteTS.isnot(None)) + pop_subq = ( + db.get_session() + .query( + coalesce(_sum(func.pow(0.98, (now - PackageVote.VoteTS) / 86400)), 0.0), + ) + .select_from(PackageVote) + .filter( + and_( + PackageVote.PackageBaseID == PackageBase.ID, + PackageVote.VoteTS.isnot(None), + ) + ) ) with db.begin(): @@ -41,17 +47,30 @@ def run_variable(pkgbases: List[PackageBase] = []) -> None: ids = set() if pkgbases: + # If `pkgbases` were given, we should forcefully update the given + # package base records' popularities. ids = {pkgbase.ID for pkgbase in pkgbases} query = query.filter(PackageBase.ID.in_(ids)) + else: + # Otherwise, we should only update popularities which have exceeded + # the popularity interval length. + interval = config.getint("git-archive", "popularity-interval") + query = query.filter( + PackageBase.PopularityUpdated + <= datetime.fromtimestamp((now - interval)) + ) - query.update({ - "NumVotes": votes_subq.scalar_subquery(), - "Popularity": pop_subq.scalar_subquery() - }) + query.update( + { + "NumVotes": votes_subq.scalar_subquery(), + "Popularity": pop_subq.scalar_subquery(), + "PopularityUpdated": datetime.fromtimestamp(now), + } + ) def run_single(pkgbase: PackageBase) -> None: - """ A single popupdate. The given pkgbase instance will be + """A single popupdate. The given pkgbase instance will be refreshed after the database update is done. NOTE: This function is compatible only with aurweb FastAPI. @@ -67,5 +86,5 @@ def main(): run_variable() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/aurweb/scripts/rendercomment.py b/aurweb/scripts/rendercomment.py index 87f8b89f..7ff477b7 100755 --- a/aurweb/scripts/rendercomment.py +++ b/aurweb/scripts/rendercomment.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import sys - from urllib.parse import quote_plus from xml.etree.ElementTree import Element @@ -10,11 +9,10 @@ import markdown import pygit2 import aurweb.config - -from aurweb import db, logging, util +from aurweb import aur_logging, db, util from aurweb.models import PackageComment -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) class LinkifyExtension(markdown.extensions.Extension): @@ -25,13 +23,15 @@ class LinkifyExtension(markdown.extensions.Extension): # Captures http(s) and ftp URLs until the first non URL-ish character. # Excludes trailing punctuation. - _urlre = (r'(\b(?:https?|ftp):\/\/[\w\/\#~:.?+=&%@!\-;,]+?' - r'(?=[.:?\-;,]*(?:[^\w\/\#~:.?+=&%@!\-;,]|$)))') + _urlre = ( + r"(\b(?:https?|ftp):\/\/[\w\/\#~:.?+=&%@!\-;,]+?" + r"(?=[.:?\-;,]*(?:[^\w\/\#~:.?+=&%@!\-;,]|$)))" + ) def extendMarkdown(self, md): processor = markdown.inlinepatterns.AutolinkInlineProcessor(self._urlre, md) # Register it right after the default <>-link processor (priority 120). - md.inlinePatterns.register(processor, 'linkify', 119) + md.inlinePatterns.register(processor, "linkify", 119) class FlysprayLinksInlineProcessor(markdown.inlinepatterns.InlineProcessor): @@ -43,16 +43,16 @@ class FlysprayLinksInlineProcessor(markdown.inlinepatterns.InlineProcessor): """ def handleMatch(self, m, data): - el = Element('a') - el.set('href', f'https://bugs.archlinux.org/task/{m.group(1)}') + el = Element("a") + el.set("href", f"https://bugs.archlinux.org/task/{m.group(1)}") el.text = markdown.util.AtomicString(m.group(0)) - return (el, m.start(0), m.end(0)) + return el, m.start(0), m.end(0) class FlysprayLinksExtension(markdown.extensions.Extension): def extendMarkdown(self, md): - processor = FlysprayLinksInlineProcessor(r'\bFS#(\d+)\b', md) - md.inlinePatterns.register(processor, 'flyspray-links', 118) + processor = FlysprayLinksInlineProcessor(r"\bFS#(\d+)\b", md) + md.inlinePatterns.register(processor, "flyspray-links", 118) class GitCommitsInlineProcessor(markdown.inlinepatterns.InlineProcessor): @@ -65,26 +65,30 @@ class GitCommitsInlineProcessor(markdown.inlinepatterns.InlineProcessor): """ def __init__(self, md, head): - repo_path = aurweb.config.get('serve', 'repo-path') + repo_path = aurweb.config.get("serve", "repo-path") self._repo = pygit2.Repository(repo_path) self._head = head - super().__init__(r'\b([0-9a-f]{7,40})\b', md) + super().__init__(r"\b([0-9a-f]{7,40})\b", md) def handleMatch(self, m, data): oid = m.group(1) - if oid not in self._repo: - # Unknown OID; preserve the orginal text. - return (None, None, None) + # Lookup might raise ValueError in case multiple object ID's were found + try: + if oid not in self._repo: + # Unknown OID; preserve the orginal text. + return None, None, None + except ValueError: + # Multiple OID's found; preserve the orginal text. + return None, None, None - el = Element('a') + el = Element("a") commit_uri = aurweb.config.get("options", "commit_uri") prefixlen = util.git_search(self._repo, oid) - el.set('href', commit_uri % ( - quote_plus(self._head), - quote_plus(oid[:prefixlen]) - )) + el.set( + "href", commit_uri % (quote_plus(self._head), quote_plus(oid[:prefixlen])) + ) el.text = markdown.util.AtomicString(oid[:prefixlen]) - return (el, m.start(0), m.end(0)) + return el, m.start(0), m.end(0) class GitCommitsExtension(markdown.extensions.Extension): @@ -97,7 +101,7 @@ class GitCommitsExtension(markdown.extensions.Extension): def extendMarkdown(self, md): try: processor = GitCommitsInlineProcessor(md, self._head) - md.inlinePatterns.register(processor, 'git-commits', 117) + md.inlinePatterns.register(processor, "git-commits", 117) except pygit2.GitError: logger.error(f"No git repository found for '{self._head}'.") @@ -105,16 +109,30 @@ class GitCommitsExtension(markdown.extensions.Extension): class HeadingTreeprocessor(markdown.treeprocessors.Treeprocessor): def run(self, doc): for elem in doc: - if elem.tag == 'h1': - elem.tag = 'h5' - elif elem.tag in ['h2', 'h3', 'h4', 'h5']: - elem.tag = 'h6' + if elem.tag == "h1": + elem.tag = "h5" + elif elem.tag in ["h2", "h3", "h4", "h5"]: + elem.tag = "h6" class HeadingExtension(markdown.extensions.Extension): def extendMarkdown(self, md): # Priority doesn't matter since we don't conflict with other processors. - md.treeprocessors.register(HeadingTreeprocessor(md), 'heading', 30) + md.treeprocessors.register(HeadingTreeprocessor(md), "heading", 30) + + +class StrikethroughInlineProcessor(markdown.inlinepatterns.InlineProcessor): + def handleMatch(self, m, data): + el = Element("del") + el.text = m.group(1) + return el, m.start(0), m.end(0) + + +class StrikethroughExtension(markdown.extensions.Extension): + def extendMarkdown(self, md): + pattern = r"~~(.*?)~~" + processor = StrikethroughInlineProcessor(pattern, md) + md.inlinePatterns.register(processor, "del", 40) def save_rendered_comment(comment: PackageComment, html: str): @@ -130,16 +148,31 @@ def update_comment_render(comment: PackageComment) -> None: text = comment.Comments pkgbasename = comment.PackageBase.Name - html = markdown.markdown(text, extensions=[ - 'fenced_code', - LinkifyExtension(), - FlysprayLinksExtension(), - GitCommitsExtension(pkgbasename), - HeadingExtension() - ]) + html = markdown.markdown( + text, + extensions=[ + "md_in_html", + "fenced_code", + LinkifyExtension(), + FlysprayLinksExtension(), + GitCommitsExtension(pkgbasename), + HeadingExtension(), + StrikethroughExtension(), + ], + ) - allowed_tags = (bleach.sanitizer.ALLOWED_TAGS - + ['p', 'pre', 'h4', 'h5', 'h6', 'br', 'hr']) + allowed_tags = list(bleach.sanitizer.ALLOWED_TAGS) + [ + "p", + "pre", + "h4", + "h5", + "h6", + "br", + "hr", + "del", + "details", + "summary", + ] html = bleach.clean(html, tags=allowed_tags) save_rendered_comment(comment, html) db.refresh(comment) @@ -148,11 +181,9 @@ def update_comment_render(comment: PackageComment) -> None: def main(): db.get_engine() comment_id = int(sys.argv[1]) - comment = db.query(PackageComment).filter( - PackageComment.ID == comment_id - ).first() + comment = db.query(PackageComment).filter(PackageComment.ID == comment_id).first() update_comment_render(comment) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/aurweb/scripts/tuvotereminder.py b/aurweb/scripts/tuvotereminder.py deleted file mode 100755 index 742fa6d4..00000000 --- a/aurweb/scripts/tuvotereminder.py +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env python3 - -from sqlalchemy import and_ - -import aurweb.config - -from aurweb import db, time -from aurweb.models import TUVoteInfo -from aurweb.scripts import notify - -notify_cmd = aurweb.config.get('notifications', 'notify-cmd') - - -def main(): - db.get_engine() - - now = time.utcnow() - - start = aurweb.config.getint("tuvotereminder", "range_start") - filter_from = now + start - - end = aurweb.config.getint("tuvotereminder", "range_end") - filter_to = now + end - - query = db.query(TUVoteInfo.ID).filter( - and_(TUVoteInfo.End >= filter_from, - TUVoteInfo.End <= filter_to) - ) - for voteinfo in query: - notif = notify.TUVoteReminderNotification(voteinfo.ID) - notif.send() - - -if __name__ == '__main__': - main() diff --git a/aurweb/scripts/usermaint.py b/aurweb/scripts/usermaint.py index 69f9db04..fb79aeaf 100755 --- a/aurweb/scripts/usermaint.py +++ b/aurweb/scripts/usermaint.py @@ -9,14 +9,16 @@ from aurweb.models import User def _main(): limit_to = time.utcnow() - 86400 * 7 - update_ = update(User).where( - User.LastLogin < limit_to - ).values(LastLoginIPAddress=None) + update_ = ( + update(User).where(User.LastLogin < limit_to).values(LastLoginIPAddress=None) + ) db.get_session().execute(update_) - update_ = update(User).where( - User.LastSSHLogin < limit_to - ).values(LastSSHLoginIPAddress=None) + update_ = ( + update(User) + .where(User.LastSSHLogin < limit_to) + .values(LastSSHLoginIPAddress=None) + ) db.get_session().execute(update_) @@ -26,5 +28,5 @@ def main(): _main() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/aurweb/scripts/votereminder.py b/aurweb/scripts/votereminder.py new file mode 100755 index 00000000..7d5c0c3b --- /dev/null +++ b/aurweb/scripts/votereminder.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +from sqlalchemy import and_ + +import aurweb.config +from aurweb import db, time +from aurweb.models import VoteInfo +from aurweb.scripts import notify + +notify_cmd = aurweb.config.get("notifications", "notify-cmd") + + +def main(): + db.get_engine() + + now = time.utcnow() + + start = aurweb.config.getint("votereminder", "range_start") + filter_from = now + start + + end = aurweb.config.getint("votereminder", "range_end") + filter_to = now + end + + query = db.query(VoteInfo.ID).filter( + and_(VoteInfo.End >= filter_from, VoteInfo.End <= filter_to) + ) + for voteinfo in query: + notif = notify.VoteReminderNotification(voteinfo.ID) + notif.send() + + +if __name__ == "__main__": + main() diff --git a/aurweb/spawn.py b/aurweb/spawn.py index 46f2f021..cfad54e1 100644 --- a/aurweb/spawn.py +++ b/aurweb/spawn.py @@ -7,7 +7,6 @@ This module uses a global state, since you can’t open two servers with the sam configuration anyway. """ - import argparse import atexit import os @@ -16,23 +15,17 @@ import subprocess import sys import tempfile import time - -from typing import Iterable, List +from typing import Iterable import aurweb.config import aurweb.schema -from aurweb.exceptions import AurwebException - children = [] temporary_dir = None verbosity = 0 -asgi_backend = '' +asgi_backend = "" workers = 1 -PHP_BINARY = os.environ.get("PHP_BINARY", "php") -PHP_MODULES = ["pdo_mysql", "pdo_sqlite"] -PHP_NGINX_PORT = int(os.environ.get("PHP_NGINX_PORT", 8001)) FASTAPI_NGINX_PORT = int(os.environ.get("FASTAPI_NGINX_PORT", 8002)) @@ -49,90 +42,55 @@ class ProcessExceptions(Exception): super().__init__("\n- ".join(messages)) -def validate_php_config() -> None: - """ - Perform a validation check against PHP_BINARY's configuration. - - AurwebException is raised here if checks fail to pass. We require - the 'pdo_mysql' and 'pdo_sqlite' modules to be enabled. - - :raises: AurwebException - :return: None - """ - try: - proc = subprocess.Popen([PHP_BINARY, "-m"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE) - out, _ = proc.communicate() - except FileNotFoundError: - raise AurwebException(f"Unable to locate the '{PHP_BINARY}' " - "executable.") - - assert proc.returncode == 0, ("Received non-zero error code " - f"{proc.returncode} from '{PHP_BINARY}'.") - - modules = out.decode().splitlines() - for module in PHP_MODULES: - if module not in modules: - raise AurwebException( - f"PHP does not have the '{module}' module enabled.") - - def generate_nginx_config(): """ Generate an nginx configuration based on aurweb's configuration. The file is generated under `temporary_dir`. Returns the path to the created configuration file. """ - php_bind = aurweb.config.get("php", "bind_address") - php_host = php_bind.split(":")[0] fastapi_bind = aurweb.config.get("fastapi", "bind_address") fastapi_host = fastapi_bind.split(":")[0] config_path = os.path.join(temporary_dir, "nginx.conf") - config = open(config_path, "w") - # We double nginx's braces because they conflict with Python's f-strings. - config.write(f""" - events {{}} - daemon off; - error_log /dev/stderr info; - pid {os.path.join(temporary_dir, "nginx.pid")}; - http {{ - access_log /dev/stdout; - client_body_temp_path {os.path.join(temporary_dir, "client_body")}; - proxy_temp_path {os.path.join(temporary_dir, "proxy")}; - fastcgi_temp_path {os.path.join(temporary_dir, "fastcgi")}1 2; - uwsgi_temp_path {os.path.join(temporary_dir, "uwsgi")}; - scgi_temp_path {os.path.join(temporary_dir, "scgi")}; - server {{ - listen {php_host}:{PHP_NGINX_PORT}; - location / {{ - proxy_pass http://{php_bind}; + with open(config_path, "w") as config: + # We double nginx's braces because they conflict with Python's f-strings. + config.write( + f""" + events {{}} + daemon off; + error_log /dev/stderr info; + pid {os.path.join(temporary_dir, "nginx.pid")}; + http {{ + access_log /dev/stdout; + client_body_temp_path {os.path.join(temporary_dir, "client_body")}; + proxy_temp_path {os.path.join(temporary_dir, "proxy")}; + fastcgi_temp_path {os.path.join(temporary_dir, "fastcgi")}1 2; + uwsgi_temp_path {os.path.join(temporary_dir, "uwsgi")}; + scgi_temp_path {os.path.join(temporary_dir, "scgi")}; + server {{ + listen {fastapi_host}:{FASTAPI_NGINX_PORT}; + location / {{ + try_files $uri @proxy_to_app; + }} + location @proxy_to_app {{ + proxy_set_header Host $http_host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_redirect off; + proxy_buffering off; + proxy_pass http://{fastapi_bind}; + }} }} }} - server {{ - listen {fastapi_host}:{FASTAPI_NGINX_PORT}; - location / {{ - try_files $uri @proxy_to_app; - }} - location @proxy_to_app {{ - proxy_set_header Host $http_host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_redirect off; - proxy_buffering off; - proxy_pass http://{fastapi_bind}; - }} - }} - }} - """) + """ + ) return config_path -def spawn_child(args): +def spawn_child(_args): """Open a subprocess and add it to the global state.""" if verbosity >= 1: - print(f":: Spawning {args}", file=sys.stderr) - children.append(subprocess.Popen(args)) + print(f":: Spawning {_args}", file=sys.stderr) + children.append(subprocess.Popen(_args)) def start(): @@ -146,30 +104,28 @@ def start(): return atexit.register(stop) - if 'AUR_CONFIG' in os.environ: - os.environ['AUR_CONFIG'] = os.path.realpath(os.environ['AUR_CONFIG']) + if "AUR_CONFIG" in os.environ: + os.environ["AUR_CONFIG"] = os.path.realpath(os.environ["AUR_CONFIG"]) try: terminal_width = os.get_terminal_size().columns except OSError: terminal_width = 80 - print("{ruler}\n" - "Spawing PHP and FastAPI, then nginx as a reverse proxy.\n" - "Check out {aur_location}\n" - "Hit ^C to terminate everything.\n" - "{ruler}" - .format(ruler=("-" * terminal_width), - aur_location=aurweb.config.get('options', 'aur_location'))) - - # PHP - php_address = aurweb.config.get("php", "bind_address") - php_host = php_address.split(":")[0] - htmldir = aurweb.config.get("php", "htmldir") - spawn_child(["php", "-S", php_address, "-t", htmldir]) + print( + "{ruler}\n" + "Spawing FastAPI, then nginx as a reverse proxy.\n" + "Check out {aur_location}\n" + "Hit ^C to terminate everything.\n" + "{ruler}".format( + ruler=("-" * terminal_width), + aur_location=aurweb.config.get("options", "aur_location"), + ) + ) # FastAPI - fastapi_host, fastapi_port = aurweb.config.get( - "fastapi", "bind_address").rsplit(":", 1) + fastapi_host, fastapi_port = aurweb.config.get("fastapi", "bind_address").rsplit( + ":", 1 + ) # Logging config. aurwebdir = aurweb.config.get("options", "aurwebdir") @@ -178,42 +134,54 @@ def start(): backend_args = { "hypercorn": ["-b", f"{fastapi_host}:{fastapi_port}"], "uvicorn": ["--host", fastapi_host, "--port", fastapi_port], - "gunicorn": ["--bind", f"{fastapi_host}:{fastapi_port}", - "-k", "uvicorn.workers.UvicornWorker", - "-w", str(workers)] + "gunicorn": [ + "--bind", + f"{fastapi_host}:{fastapi_port}", + "-k", + "uvicorn.workers.UvicornWorker", + "-w", + str(workers), + ], } backend_args = backend_args.get(asgi_backend) - spawn_child([ - "python", "-m", asgi_backend, - "--log-config", fastapi_log_config, - ] + backend_args + ["aurweb.asgi:app"]) + spawn_child( + [ + "python", + "-m", + asgi_backend, + "--log-config", + fastapi_log_config, + ] + + backend_args + + ["aurweb.asgi:app"] + ) # nginx spawn_child(["nginx", "-p", temporary_dir, "-c", generate_nginx_config()]) - print(f""" + print( + f""" > Started nginx. > - > PHP backend: http://{php_address} - > FastAPI backend: http://{fastapi_host}:{fastapi_port} - > - > PHP frontend: http://{php_host}:{PHP_NGINX_PORT} + > FastAPI backend: http://{fastapi_host}:{fastapi_port} > FastAPI frontend: http://{fastapi_host}:{FASTAPI_NGINX_PORT} > > Frontends are hosted via nginx and should be preferred. -""") +""" + ) -def _kill_children(children: Iterable, exceptions: List[Exception] = []) \ - -> List[Exception]: +def _kill_children(_children: Iterable, exceptions=None) -> list[Exception]: """ Kill each process found in `children`. - :param children: Iterable of child processes + :param _children: Iterable of child processes :param exceptions: Exception memo :return: `exceptions` """ - for p in children: + if exceptions is None: + exceptions = [] + for p in _children: try: p.terminate() if verbosity >= 1: @@ -223,16 +191,17 @@ def _kill_children(children: Iterable, exceptions: List[Exception] = []) \ return exceptions -def _wait_for_children(children: Iterable, exceptions: List[Exception] = []) \ - -> List[Exception]: +def _wait_for_children(_children: Iterable, exceptions=None) -> list[Exception]: """ Wait for each process to end found in `children`. - :param children: Iterable of child processes + :param _children: Iterable of child processes :param exceptions: Exception memo :return: `exceptions` """ - for p in children: + if exceptions is None: + exceptions = [] + for p in _children: try: rc = p.wait() if rc != 0 and rc != -15: @@ -261,29 +230,33 @@ def stop() -> None: exceptions = _wait_for_children(children, exceptions) children = [] if exceptions: - raise ProcessExceptions("Errors terminating the child processes:", - exceptions) + raise ProcessExceptions("Errors terminating the child processes:", exceptions) -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser( - prog='python -m aurweb.spawn', - description='Start aurweb\'s test server.') - parser.add_argument('-v', '--verbose', action='count', default=0, - help='increase verbosity') - choices = ['hypercorn', 'gunicorn', 'uvicorn'] - parser.add_argument('-b', '--backend', choices=choices, default='uvicorn', - help='asgi backend used to launch the python server') - parser.add_argument("-w", "--workers", default=1, type=int, - help="number of workers to use in gunicorn") + prog="python -m aurweb.spawn", description="Start aurweb's test server." + ) + parser.add_argument( + "-v", "--verbose", action="count", default=0, help="increase verbosity" + ) + choices = ["hypercorn", "gunicorn", "uvicorn"] + parser.add_argument( + "-b", + "--backend", + choices=choices, + default="uvicorn", + help="asgi backend used to launch the python server", + ) + parser.add_argument( + "-w", + "--workers", + default=1, + type=int, + help="number of workers to use in gunicorn", + ) args = parser.parse_args() - try: - validate_php_config() - except AurwebException as exc: - print(f"error: {str(exc)}") - sys.exit(1) - verbosity = args.verbose asgi_backend = args.backend workers = args.workers diff --git a/aurweb/statistics.py b/aurweb/statistics.py new file mode 100644 index 00000000..00a5c151 --- /dev/null +++ b/aurweb/statistics.py @@ -0,0 +1,169 @@ +from sqlalchemy import func + +from aurweb import config, db, time +from aurweb.cache import db_count_cache, db_query_cache +from aurweb.models import PackageBase, PackageRequest, RequestType, User +from aurweb.models.account_type import ( + PACKAGE_MAINTAINER_AND_DEV_ID, + PACKAGE_MAINTAINER_ID, + USER_ID, +) +from aurweb.models.package_request import ( + ACCEPTED_ID, + CLOSED_ID, + PENDING_ID, + REJECTED_ID, +) +from aurweb.prometheus import PACKAGES, REQUESTS, USERS + +cache_expire = config.getint("cache", "expiry_time_statistics", 300) + +HOMEPAGE_COUNTERS = [ + "package_count", + "orphan_count", + "seven_days_old_added", + "seven_days_old_updated", + "year_old_updated", + "never_updated", + "user_count", + "package_maintainer_count", +] +REQUEST_COUNTERS = [ + "total_requests", + "pending_requests", + "closed_requests", + "accepted_requests", + "rejected_requests", +] +PROMETHEUS_USER_COUNTERS = [ + ("package_maintainer_count", "package_maintainer"), + ("regular_user_count", "user"), +] +PROMETHEUS_PACKAGE_COUNTERS = [ + ("orphan_count", "orphan"), + ("never_updated", "not_updated"), + ("updated_packages", "updated"), +] + + +class Statistics: + seven_days = 86400 * 7 + one_hour = 3600 + year = seven_days * 52 + + def __init__(self, cache_expire: int = None) -> "Statistics": + self.expiry_time = cache_expire + self.now = time.utcnow() + self.seven_days_ago = self.now - self.seven_days + self.year_ago = self.now - self.year + + self.user_query = db.query(User) + self.bases_query = db.query(PackageBase) + self.updated_query = db.query(PackageBase).filter( + PackageBase.ModifiedTS - PackageBase.SubmittedTS >= self.one_hour + ) + self.request_query = db.query(PackageRequest) + + def get_count(self, counter: str) -> int: + query = None + match counter: + # Packages + case "package_count": + query = self.bases_query + case "orphan_count": + query = self.bases_query.filter(PackageBase.MaintainerUID.is_(None)) + case "seven_days_old_added": + query = self.bases_query.filter( + PackageBase.SubmittedTS >= self.seven_days_ago + ) + case "seven_days_old_updated": + query = self.updated_query.filter( + PackageBase.ModifiedTS >= self.seven_days_ago + ) + case "year_old_updated": + query = self.updated_query.filter( + PackageBase.ModifiedTS >= self.year_ago + ) + case "never_updated": + query = self.bases_query.filter( + PackageBase.ModifiedTS - PackageBase.SubmittedTS < self.one_hour + ) + case "updated_packages": + query = self.bases_query.filter( + PackageBase.ModifiedTS - PackageBase.SubmittedTS > self.one_hour, + ~PackageBase.MaintainerUID.is_(None), + ) + # Users + case "user_count": + query = self.user_query + case "package_maintainer_count": + query = self.user_query.filter( + User.AccountTypeID.in_( + ( + PACKAGE_MAINTAINER_ID, + PACKAGE_MAINTAINER_AND_DEV_ID, + ) + ) + ) + case "regular_user_count": + query = self.user_query.filter(User.AccountTypeID == USER_ID) + + # Requests + case "total_requests": + query = self.request_query + case "pending_requests": + query = self.request_query.filter(PackageRequest.Status == PENDING_ID) + case "closed_requests": + query = self.request_query.filter(PackageRequest.Status == CLOSED_ID) + case "accepted_requests": + query = self.request_query.filter(PackageRequest.Status == ACCEPTED_ID) + case "rejected_requests": + query = self.request_query.filter(PackageRequest.Status == REJECTED_ID) + case _: + return -1 + + return db_count_cache(counter, query, expire=self.expiry_time) + + +def update_prometheus_metrics(): + stats = Statistics(cache_expire) + # Users gauge + for counter, utype in PROMETHEUS_USER_COUNTERS: + count = stats.get_count(counter) + USERS.labels(utype).set(count) + + # Packages gauge + for counter, state in PROMETHEUS_PACKAGE_COUNTERS: + count = stats.get_count(counter) + PACKAGES.labels(state).set(count) + + # Requests gauge + query = ( + db.get_session() + .query(PackageRequest, func.count(PackageRequest.ID), RequestType.Name) + .join(RequestType) + .group_by(RequestType.Name, PackageRequest.Status) + ) + results = db_query_cache("request_metrics", query, cache_expire) + for record in results: + status = record[0].status_display() + count = record[1] + rtype = record[2] + REQUESTS.labels(type=rtype, status=status).set(count) + + +def _get_counts(counters: list[str]) -> dict[str, int]: + stats = Statistics(cache_expire) + result = dict() + for counter in counters: + result[counter] = stats.get_count(counter) + + return result + + +def get_homepage_counts() -> dict[str, int]: + return _get_counts(HOMEPAGE_COUNTERS) + + +def get_request_counts() -> dict[str, int]: + return _get_counts(REQUEST_COUNTERS) diff --git a/aurweb/templates.py b/aurweb/templates.py index ccadb16d..51b9d342 100644 --- a/aurweb/templates.py +++ b/aurweb/templates.py @@ -1,28 +1,29 @@ import copy import functools import os - from http import HTTPStatus from typing import Callable import jinja2 - from fastapi import Request from fastapi.responses import HTMLResponse import aurweb.config - -from aurweb import cookies, l10n, time +from aurweb import l10n, time # Prepare jinja2 objects. -_loader = jinja2.FileSystemLoader(os.path.join( - aurweb.config.get("options", "aurwebdir"), "templates")) -_env = jinja2.Environment(loader=_loader, autoescape=True, - extensions=["jinja2.ext.i18n"]) +_loader = jinja2.FileSystemLoader( + os.path.join(aurweb.config.get("options", "aurwebdir"), "templates") +) +_env = jinja2.Environment( + loader=_loader, autoescape=True, extensions=["jinja2.ext.i18n"] +) + +DEFAULT_TIMEZONE = aurweb.config.get("options", "default_timezone") def register_filter(name: str) -> Callable: - """ A decorator that can be used to register a filter. + """A decorator that can be used to register a filter. Example @register_filter("some_filter") @@ -35,35 +36,41 @@ def register_filter(name: str) -> Callable: :param name: Filter name :return: Callable used for filter """ + def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) + _env.filters[name] = wrapper return wrapper + return decorator def register_function(name: str) -> Callable: - """ A decorator that can be used to register a function. - """ + """A decorator that can be used to register a function.""" + def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) + if name in _env.globals: raise KeyError(f"Jinja already has a function named '{name}'") _env.globals[name] = wrapper return wrapper + return decorator def make_context(request: Request, title: str, next: str = None): - """ Create a context for a jinja2 TemplateResponse. """ + """Create a context for a jinja2 TemplateResponse.""" import aurweb.auth.creds commit_url = aurweb.config.get_with_fallback("devel", "commit_url", None) commit_hash = aurweb.config.get_with_fallback("devel", "commit_hash", None) + max_chars_comment = aurweb.config.getint("options", "max_chars_comment", 5000) if commit_hash: # Shorten commit_hash to a short Git hash. commit_hash = commit_hash[:7] @@ -85,20 +92,25 @@ def make_context(request: Request, title: str, next: str = None): "config": aurweb.config, "creds": aurweb.auth.creds, "next": next if next else request.url.path, - "version": os.environ.get("COMMIT_HASH", aurweb.config.AURWEB_VERSION) + "version": os.environ.get("COMMIT_HASH", aurweb.config.AURWEB_VERSION), + "max_chars_comment": max_chars_comment, } async def make_variable_context(request: Request, title: str, next: str = None): - """ Make a context with variables provided by the user - (query params via GET or form data via POST). """ + """Make a context with variables provided by the user + (query params via GET or form data via POST).""" context = make_context(request, title, next) - to_copy = dict(request.query_params) \ - if request.method.lower() == "get" \ + to_copy = ( + dict(request.query_params) + if request.method.lower() == "get" else dict(await request.form()) + ) for k, v in to_copy.items(): - context[k] = v + if k not in context: + context[k] = v + context["q"] = dict(request.query_params) return context @@ -109,7 +121,7 @@ def base_template(path: str): def render_raw_template(request: Request, path: str, context: dict): - """ Render a Jinja2 multi-lingual template with some context. """ + """Render a Jinja2 multi-lingual template with some context.""" # Create a deep copy of our jinja2 _environment. The _environment in # total by itself is 48 bytes large (according to sys.getsizeof). # This is done so we can install gettext translations on the template @@ -124,19 +136,9 @@ def render_raw_template(request: Request, path: str, context: dict): return template.render(context) -def render_template(request: Request, - path: str, - context: dict, - status_code: HTTPStatus = HTTPStatus.OK): - """ Render a template as an HTMLResponse. """ +def render_template( + request: Request, path: str, context: dict, status_code: HTTPStatus = HTTPStatus.OK +): + """Render a template as an HTMLResponse.""" rendered = render_raw_template(request, path, context) - response = HTMLResponse(rendered, status_code=int(status_code)) - - sid = None - if request.user.is_authenticated(): - sid = request.cookies.get("AURSID") - - # Re-emit SID via update_response_cookies with an updated expiration. - # This extends the life of a user session based on the AURREMEMBER - # cookie, which is always set to the "Remember Me" state on login. - return cookies.update_response_cookies(request, response, aursid=sid) + return HTMLResponse(rendered, status_code=int(status_code)) diff --git a/aurweb/testing/__init__.py b/aurweb/testing/__init__.py index 8261051d..b9b1d263 100644 --- a/aurweb/testing/__init__.py +++ b/aurweb/testing/__init__.py @@ -1,10 +1,9 @@ import aurweb.db - from aurweb import models def setup_test_db(*args): - """ This function is to be used to setup a test database before + """This function is to be used to setup a test database before using it. It takes a variable number of table strings, and for each table in that set of table strings, it deletes all records. @@ -52,8 +51,8 @@ def setup_test_db(*args): models.Session.__tablename__, models.SSHPubKey.__tablename__, models.Term.__tablename__, - models.TUVote.__tablename__, - models.TUVoteInfo.__tablename__, + models.Vote.__tablename__, + models.VoteInfo.__tablename__, models.User.__tablename__, ] diff --git a/aurweb/testing/alpm.py b/aurweb/testing/alpm.py index 6015d859..61a9315f 100644 --- a/aurweb/testing/alpm.py +++ b/aurweb/testing/alpm.py @@ -4,12 +4,10 @@ import re import shutil import subprocess -from typing import List - -from aurweb import logging, util +from aurweb import aur_logging, util from aurweb.templates import base_template -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) class AlpmDatabase: @@ -19,6 +17,7 @@ class AlpmDatabase: This class can be used to add or remove packages from a test repository. """ + repo = "test" def __init__(self, database_root: str): @@ -37,13 +36,14 @@ class AlpmDatabase: os.makedirs(pkgdir) return pkgdir - def add(self, pkgname: str, pkgver: str, arch: str, - provides: List[str] = []) -> None: + def add( + self, pkgname: str, pkgver: str, arch: str, provides: list[str] = [] + ) -> None: context = { "pkgname": pkgname, "pkgver": pkgver, "arch": arch, - "provides": provides + "provides": provides, } template = base_template("testing/alpm_package.j2") pkgdir = self._get_pkgdir(pkgname, pkgver, self.repo) @@ -78,8 +78,9 @@ class AlpmDatabase: self.clean() cmdline = ["bash", "-c", "bsdtar -czvf ../test.db *"] proc = subprocess.run(cmdline, cwd=self.repopath) - assert proc.returncode == 0, \ - f"Bad return code while creating alpm database: {proc.returncode}" + assert ( + proc.returncode == 0 + ), f"Bad return code while creating alpm database: {proc.returncode}" # Print out the md5 hash value of the new test.db. test_db = os.path.join(self.remote, "test.db") diff --git a/aurweb/testing/email.py b/aurweb/testing/email.py index b3e3990b..057ff792 100644 --- a/aurweb/testing/email.py +++ b/aurweb/testing/email.py @@ -5,7 +5,6 @@ import email import os import re import sys - from typing import TextIO @@ -28,6 +27,7 @@ class Email: print(email.headers) """ + TEST_DIR = "test-emails" def __init__(self, serial: int = 1, autoparse: bool = True): @@ -61,7 +61,7 @@ class Email: value = os.environ.get("PYTEST_CURRENT_TEST", "email").split(" ")[0] if suite: value = value.split(":")[0] - return re.sub(r'(\/|\.|,|:)', "_", value) + return re.sub(r"(\/|\.|,|:)", "_", value) @staticmethod def count() -> int: @@ -159,6 +159,6 @@ class Email: lines += [ f"== Email #{i + 1} ==", email.glue(), - f"== End of Email #{i + 1}" + f"== End of Email #{i + 1}", ] print("\n".join(lines), file=file) diff --git a/aurweb/testing/filelock.py b/aurweb/testing/filelock.py index 3a18c153..d582f0bf 100644 --- a/aurweb/testing/filelock.py +++ b/aurweb/testing/filelock.py @@ -1,13 +1,12 @@ import hashlib import os - from typing import Callable from posix_ipc import O_CREAT, Semaphore -from aurweb import logging +from aurweb import aur_logging -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) def default_on_create(path): diff --git a/aurweb/testing/git.py b/aurweb/testing/git.py index 019d870f..39af87de 100644 --- a/aurweb/testing/git.py +++ b/aurweb/testing/git.py @@ -1,7 +1,4 @@ import os -import shlex - -from subprocess import PIPE, Popen from typing import Tuple import py @@ -9,6 +6,7 @@ import py from aurweb.models import Package from aurweb.templates import base_template from aurweb.testing.filelock import FileLock +from aurweb.util import shell_exec class GitRepository: @@ -25,10 +23,7 @@ class GitRepository: self.file_lock.lock(on_create=self._setup) def _exec(self, cmdline: str, cwd: str) -> Tuple[int, str, str]: - args = shlex.split(cmdline) - proc = Popen(args, cwd=cwd, stdout=PIPE, stderr=PIPE) - out, err = proc.communicate() - return (proc.returncode, out.decode().strip(), err.decode().strip()) + return shell_exec(cmdline, cwd) def _exec_repository(self, cmdline: str) -> Tuple[int, str, str]: return self._exec(cmdline, cwd=str(self.file_lock.path)) diff --git a/aurweb/testing/html.py b/aurweb/testing/html.py index f01aaf3d..16b7322b 100644 --- a/aurweb/testing/html.py +++ b/aurweb/testing/html.py @@ -1,5 +1,4 @@ from io import StringIO -from typing import List from lxml import etree @@ -7,7 +6,7 @@ parser = etree.HTMLParser() def parse_root(html: str) -> etree.Element: - """ Parse an lxml.etree.ElementTree root from html content. + """Parse an lxml.etree.ElementTree root from html content. :param html: HTML markup :return: etree.Element @@ -15,11 +14,11 @@ def parse_root(html: str) -> etree.Element: return etree.parse(StringIO(html), parser) -def get_errors(content: str) -> List[etree._Element]: +def get_errors(content: str) -> list[etree._Element]: root = parse_root(content) return root.xpath('//ul[@class="errorlist"]/li') -def get_successes(content: str) -> List[etree._Element]: +def get_successes(content: str) -> list[etree._Element]: root = parse_root(content) return root.xpath('//ul[@class="success"]/li') diff --git a/aurweb/testing/prometheus.py b/aurweb/testing/prometheus.py new file mode 100644 index 00000000..d04190f6 --- /dev/null +++ b/aurweb/testing/prometheus.py @@ -0,0 +1,8 @@ +from aurweb import prometheus + + +def clear_metrics(): + prometheus.PACKAGES.clear() + prometheus.REQUESTS.clear() + prometheus.SEARCH_REQUESTS.clear() + prometheus.USERS.clear() diff --git a/aurweb/testing/requests.py b/aurweb/testing/requests.py index be13ab77..da463928 100644 --- a/aurweb/testing/requests.py +++ b/aurweb/testing/requests.py @@ -1,10 +1,9 @@ -from typing import Dict - import aurweb.config class User: - """ A fake User model. """ + """A fake User model.""" + # Fake columns. LangPreference = aurweb.config.get("options", "default_lang") Timezone = aurweb.config.get("options", "default_timezone") @@ -17,29 +16,40 @@ class User: class Client: - """ A fake FastAPI Request.client object. """ + """A fake FastAPI Request.client object.""" + # A fake host. host = "127.0.0.1" class URL: - path = "/" + path: str + + def __init__(self, path: str = "/"): + self.path = path class Request: - """ A fake Request object which mimics a FastAPI Request for tests. """ + """A fake Request object which mimics a FastAPI Request for tests.""" + client = Client() url = URL() - def __init__(self, - user: User = User(), - authenticated: bool = False, - method: str = "GET", - headers: Dict[str, str] = dict(), - cookies: Dict[str, str] = dict()) -> "Request": + def __init__( + self, + user: User = User(), + authenticated: bool = False, + method: str = "GET", + headers: dict[str, str] = dict(), + cookies: dict[str, str] = dict(), + url: str = "/", + query_params: dict[str, str] = dict(), + ) -> "Request": self.user = user self.user.authenticated = authenticated self.method = method.upper() self.headers = headers self.cookies = cookies + self.url = URL(path=url) + self.query_params = query_params diff --git a/aurweb/testing/smtp.py b/aurweb/testing/smtp.py index e5d67991..7596fbe9 100644 --- a/aurweb/testing/smtp.py +++ b/aurweb/testing/smtp.py @@ -2,7 +2,7 @@ class FakeSMTP: - """ A fake version of smtplib.SMTP used for testing. """ + """A fake version of smtplib.SMTP used for testing.""" starttls_enabled = False use_ssl = False @@ -41,5 +41,6 @@ class FakeSMTP: class FakeSMTP_SSL(FakeSMTP): - """ A fake version of smtplib.SMTP_SSL used for testing. """ + """A fake version of smtplib.SMTP_SSL used for testing.""" + use_ssl = True diff --git a/aurweb/time.py b/aurweb/time.py index a97ca986..2d5ddcc1 100644 --- a/aurweb/time.py +++ b/aurweb/time.py @@ -1,8 +1,6 @@ import zoneinfo - from collections import OrderedDict -from datetime import datetime -from urllib.parse import unquote +from datetime import UTC, datetime from zoneinfo import ZoneInfo from fastapi import Request @@ -11,7 +9,7 @@ import aurweb.config def tz_offset(name: str): - """ Get a timezone offset in the form "+00:00" by its name. + """Get a timezone offset in the form "+00:00" by its name. Example: tz_offset('America/Los_Angeles') @@ -24,7 +22,7 @@ def tz_offset(name: str): offset = dt.utcoffset().total_seconds() / 60 / 60 # Prefix the offset string with a - or +. - offset_string = '-' if offset < 0 else '+' + offset_string = "-" if offset < 0 else "+" # Remove any negativity from the offset. We want a good offset. :) offset = abs(offset) @@ -42,27 +40,37 @@ def tz_offset(name: str): return offset_string -SUPPORTED_TIMEZONES = OrderedDict({ - # Flatten out the list of tuples into an OrderedDict. - timezone: offset for timezone, offset in sorted([ - # Comprehend a list of tuples (timezone, offset display string) - # and sort them by (offset, timezone). - (tz, "(UTC%s) %s" % (tz_offset(tz), tz)) - for tz in zoneinfo.available_timezones() - ], key=lambda element: (tz_offset(element[0]), element[0])) -}) +SUPPORTED_TIMEZONES = OrderedDict( + { + # Flatten out the list of tuples into an OrderedDict. + timezone: offset + for timezone, offset in sorted( + [ + # Comprehend a list of tuples (timezone, offset display string) + # and sort them by (offset, timezone). + (tz, "(UTC%s) %s" % (tz_offset(tz), tz)) + for tz in zoneinfo.available_timezones() + ], + key=lambda element: (tz_offset(element[0]), element[0]), + ) + } +) -def get_request_timezone(request: Request): - """ Get a request's timezone by its AURTZ cookie. We use the - configuration's [options] default_timezone otherwise. +def get_request_timezone(request: Request) -> str: + """Get a request's timezone from either query param or user settings. + We use the configuration's [options] default_timezone otherwise. @param request FastAPI request """ - default_tz = aurweb.config.get("options", "default_timezone") - if request.user.is_authenticated(): - default_tz = request.user.Timezone - return unquote(request.cookies.get("AURTZ", default_tz)) + request_tz = request.query_params.get("timezone") + if request_tz and request_tz in SUPPORTED_TIMEZONES: + return request_tz + elif ( + request.user.is_authenticated() and request.user.Timezone in SUPPORTED_TIMEZONES + ): + return request.user.Timezone + return aurweb.config.get_with_fallback("options", "default_timezone", "UTC") def now(timezone: str) -> datetime: @@ -81,4 +89,4 @@ def utcnow() -> int: :return: Current UTC timestamp """ - return int(datetime.utcnow().timestamp()) + return int(datetime.now(UTC).timestamp()) diff --git a/aurweb/users/update.py b/aurweb/users/update.py index 5a32fd01..759088cd 100644 --- a/aurweb/users/update.py +++ b/aurweb/users/update.py @@ -1,19 +1,32 @@ -from typing import Any, Dict +from typing import Any from fastapi import Request -from aurweb import cookies, db, models, time, util +from aurweb import db, models, time, util from aurweb.models import SSHPubKey from aurweb.models.ssh_pub_key import get_fingerprint from aurweb.util import strtobool -def simple(U: str = str(), E: str = str(), H: bool = False, - BE: str = str(), R: str = str(), HP: str = str(), - I: str = str(), K: str = str(), J: bool = False, - CN: bool = False, UN: bool = False, ON: bool = False, - S: bool = False, user: models.User = None, - **kwargs) -> None: +@db.retry_deadlock +def simple( + U: str = str(), + E: str = str(), + H: bool = False, + BE: str = str(), + R: str = str(), + HP: str = str(), + I: str = str(), + K: str = str(), + J: bool = False, + CN: bool = False, + UN: bool = False, + ON: bool = False, + HDC: bool = False, + S: bool = False, + user: models.User = None, + **kwargs, +) -> None: now = time.utcnow() with db.begin(): user.Username = U or user.Username @@ -29,30 +42,38 @@ def simple(U: str = str(), E: str = str(), H: bool = False, user.CommentNotify = strtobool(CN) user.UpdateNotify = strtobool(UN) user.OwnershipNotify = strtobool(ON) + user.HideDeletedComments = strtobool(HDC) -def language(L: str = str(), - request: Request = None, - user: models.User = None, - context: Dict[str, Any] = {}, - **kwargs) -> None: +@db.retry_deadlock +def language( + L: str = str(), + request: Request = None, + user: models.User = None, + context: dict[str, Any] = {}, + **kwargs, +) -> None: if L and L != user.LangPreference: with db.begin(): user.LangPreference = L context["language"] = L -def timezone(TZ: str = str(), - request: Request = None, - user: models.User = None, - context: Dict[str, Any] = {}, - **kwargs) -> None: +@db.retry_deadlock +def timezone( + TZ: str = str(), + request: Request = None, + user: models.User = None, + context: dict[str, Any] = {}, + **kwargs, +) -> None: if TZ and TZ != user.Timezone: with db.begin(): user.Timezone = TZ context["language"] = TZ +@db.retry_deadlock def ssh_pubkey(PK: str = str(), user: models.User = None, **kwargs) -> None: if not PK: # If no pubkey is provided, wipe out any pubkeys the user @@ -67,8 +88,7 @@ def ssh_pubkey(PK: str = str(), user: models.User = None, **kwargs) -> None: with db.begin(): # Delete any existing keys we can't find. - to_remove = user.ssh_pub_keys.filter( - ~SSHPubKey.Fingerprint.in_(fprints)) + to_remove = user.ssh_pub_keys.filter(~SSHPubKey.Fingerprint.in_(fprints)) db.delete_all(to_remove) # For each key, if it does not yet exist, create it. @@ -79,24 +99,29 @@ def ssh_pubkey(PK: str = str(), user: models.User = None, **kwargs) -> None: ).exists() if not db.query(exists).scalar(): # No public key exists, create one. - db.create(models.SSHPubKey, UserID=user.ID, - PubKey=" ".join([prefix, key]), - Fingerprint=fprints[i]) + db.create( + models.SSHPubKey, + UserID=user.ID, + PubKey=" ".join([prefix, key]), + Fingerprint=fprints[i], + ) -def account_type(T: int = None, - user: models.User = None, - **kwargs) -> None: +@db.retry_deadlock +def account_type(T: int = None, user: models.User = None, **kwargs) -> None: if T is not None and (T := int(T)) != user.AccountTypeID: with db.begin(): user.AccountTypeID = T -def password(P: str = str(), - request: Request = None, - user: models.User = None, - context: Dict[str, Any] = {}, - **kwargs) -> None: +@db.retry_deadlock +def password( + P: str = str(), + request: Request = None, + user: models.User = None, + context: dict[str, Any] = {}, + **kwargs, +) -> None: if P and not user.valid_password(P): # Remove the fields we consumed for passwords. context["P"] = context["C"] = str() @@ -106,8 +131,22 @@ def password(P: str = str(), user.update_password(P) if user == request.user: - remember_me = request.cookies.get("AURREMEMBER", False) - # If the target user is the request user, login with # the updated password to update the Session record. - user.login(request, P, cookies.timeout(remember_me)) + user.login(request, P) + + +@db.retry_deadlock +def suspend( + S: bool = False, + request: Request = None, + user: models.User = None, + context: dict[str, Any] = {}, + **kwargs, +) -> None: + if S and user.session: + context["S"] = None + with db.begin(): + db.delete_all( + db.query(models.Session).filter(models.Session.UsersID == user.ID) + ) diff --git a/aurweb/users/validate.py b/aurweb/users/validate.py index de51e3ff..81484e90 100644 --- a/aurweb/users/validate.py +++ b/aurweb/users/validate.py @@ -6,10 +6,11 @@ out of form data from /account/register or /account/{username}/edit. All functions in this module raise aurweb.exceptions.ValidationError when encountering invalid criteria and return silently otherwise. """ + from fastapi import Request from sqlalchemy import and_ -from aurweb import config, db, l10n, logging, models, time, util +from aurweb import aur_logging, config, db, l10n, models, time, util from aurweb.auth import creds from aurweb.captcha import get_captcha_answer, get_captcha_salts, get_captcha_token from aurweb.exceptions import ValidationError @@ -17,7 +18,7 @@ from aurweb.models.account_type import ACCOUNT_TYPE_NAME from aurweb.models.ssh_pub_key import get_fingerprint from aurweb.util import strtobool -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) def invalid_fields(E: str = str(), U: str = str(), **kwargs) -> None: @@ -25,42 +26,41 @@ def invalid_fields(E: str = str(), U: str = str(), **kwargs) -> None: raise ValidationError(["Missing a required field."]) -def invalid_suspend_permission(request: Request = None, - user: models.User = None, - S: str = "False", - **kwargs) -> None: +def invalid_suspend_permission( + request: Request = None, user: models.User = None, S: str = "False", **kwargs +) -> None: if not request.user.is_elevated() and strtobool(S) != bool(user.Suspended): - raise ValidationError([ - "You do not have permission to suspend accounts."]) + raise ValidationError(["You do not have permission to suspend accounts."]) -def invalid_username(request: Request = None, U: str = str(), - _: l10n.Translator = None, - **kwargs) -> None: +def invalid_username( + request: Request = None, U: str = str(), _: l10n.Translator = None, **kwargs +) -> None: if not util.valid_username(U): username_min_len = config.getint("options", "username_min_len") username_max_len = config.getint("options", "username_max_len") - raise ValidationError([ - "The username is invalid.", + raise ValidationError( [ - _("It must be between %s and %s characters long") % ( - username_min_len, username_max_len), - "Start and end with a letter or number", - "Can contain only one period, underscore or hyphen.", + "The username is invalid.", + [ + _("It must be between %s and %s characters long") + % (username_min_len, username_max_len), + "Start and end with a letter or number", + "Can contain only one period, underscore or hyphen.", + ], ] - ]) + ) -def invalid_password(P: str = str(), C: str = str(), - _: l10n.Translator = None, **kwargs) -> None: +def invalid_password( + P: str = str(), C: str = str(), _: l10n.Translator = None, **kwargs +) -> None: if P: if not util.valid_password(P): - username_min_len = config.getint( - "options", "username_min_len") - raise ValidationError([ - _("Your password must be at least %s characters.") % ( - username_min_len) - ]) + passwd_min_len = config.getint("options", "passwd_min_len") + raise ValidationError( + [_("Your password must be at least %s characters.") % (passwd_min_len)] + ) elif not C: raise ValidationError(["Please confirm your new password."]) elif P != C: @@ -68,18 +68,21 @@ def invalid_password(P: str = str(), C: str = str(), def is_banned(request: Request = None, **kwargs) -> None: - host = request.client.host + host = util.get_client_ip(request) exists = db.query(models.Ban, models.Ban.IPAddress == host).exists() if db.query(exists).scalar(): - raise ValidationError([ - "Account registration has been disabled for your " - "IP address, probably due to sustained spam attacks. " - "Sorry for the inconvenience." - ]) + raise ValidationError( + [ + "Account registration has been disabled for your " + "IP address, probably due to sustained spam attacks. " + "Sorry for the inconvenience." + ] + ) -def invalid_user_password(request: Request = None, passwd: str = str(), - **kwargs) -> None: +def invalid_user_password( + request: Request = None, passwd: str = str(), **kwargs +) -> None: if request.user.is_authenticated(): if not request.user.valid_password(passwd): raise ValidationError(["Invalid password."]) @@ -97,8 +100,9 @@ def invalid_backup_email(BE: str = str(), **kwargs) -> None: def invalid_homepage(HP: str = str(), **kwargs) -> None: if HP and not util.valid_homepage(HP): - raise ValidationError([ - "The home page is invalid, please specify the full HTTP(s) URL."]) + raise ValidationError( + ["The home page is invalid, please specify the full HTTP(s) URL."] + ) def invalid_pgp_key(K: str = str(), **kwargs) -> None: @@ -106,8 +110,9 @@ def invalid_pgp_key(K: str = str(), **kwargs) -> None: raise ValidationError(["The PGP key fingerprint is invalid."]) -def invalid_ssh_pubkey(PK: str = str(), user: models.User = None, - _: l10n.Translator = None, **kwargs) -> None: +def invalid_ssh_pubkey( + PK: str = str(), user: models.User = None, _: l10n.Translator = None, **kwargs +) -> None: if not PK: return @@ -119,15 +124,23 @@ def invalid_ssh_pubkey(PK: str = str(), user: models.User = None, for prefix, key in keys: fingerprint = get_fingerprint(f"{prefix} {key}") - exists = db.query(models.SSHPubKey).filter( - and_(models.SSHPubKey.UserID != user.ID, - models.SSHPubKey.Fingerprint == fingerprint) - ).exists() + exists = ( + db.query(models.SSHPubKey) + .filter( + and_( + models.SSHPubKey.UserID != user.ID, + models.SSHPubKey.Fingerprint == fingerprint, + ) + ) + .exists() + ) if db.query(exists).scalar(): - raise ValidationError([ - _("The SSH public key, %s%s%s, is already in use.") % ( - "", fingerprint, "") - ]) + raise ValidationError( + [ + _("The SSH public key, %s%s%s, is already in use.") + % ("", fingerprint, "") + ] + ) def invalid_language(L: str = str(), **kwargs) -> None: @@ -140,60 +153,78 @@ def invalid_timezone(TZ: str = str(), **kwargs) -> None: raise ValidationError(["Timezone is not currently supported."]) -def username_in_use(U: str = str(), user: models.User = None, - _: l10n.Translator = None, **kwargs) -> None: - exists = db.query(models.User).filter( - and_(models.User.ID != user.ID, - models.User.Username == U) - ).exists() +def username_in_use( + U: str = str(), user: models.User = None, _: l10n.Translator = None, **kwargs +) -> None: + exists = ( + db.query(models.User) + .filter(and_(models.User.ID != user.ID, models.User.Username == U)) + .exists() + ) if db.query(exists).scalar(): # If the username already exists... - raise ValidationError([ - _("The username, %s%s%s, is already in use.") % ( - "", U, "") - ]) + raise ValidationError( + [ + _("The username, %s%s%s, is already in use.") + % ("", U, "") + ] + ) -def email_in_use(E: str = str(), user: models.User = None, - _: l10n.Translator = None, **kwargs) -> None: - exists = db.query(models.User).filter( - and_(models.User.ID != user.ID, - models.User.Email == E) - ).exists() +def email_in_use( + E: str = str(), user: models.User = None, _: l10n.Translator = None, **kwargs +) -> None: + exists = ( + db.query(models.User) + .filter(and_(models.User.ID != user.ID, models.User.Email == E)) + .exists() + ) if db.query(exists).scalar(): # If the email already exists... - raise ValidationError([ - _("The address, %s%s%s, is already in use.") % ( - "", E, "") - ]) + raise ValidationError( + [ + _("The address, %s%s%s, is already in use.") + % ("", E, "") + ] + ) -def invalid_account_type(T: int = None, request: Request = None, - user: models.User = None, - _: l10n.Translator = None, - **kwargs) -> None: +def invalid_account_type( + T: int = None, + request: Request = None, + user: models.User = None, + _: l10n.Translator = None, + **kwargs, +) -> None: if T is not None and (T := int(T)) != user.AccountTypeID: name = ACCOUNT_TYPE_NAME.get(T, None) has_cred = request.user.has_credential(creds.ACCOUNT_CHANGE_TYPE) if name is None: raise ValidationError(["Invalid account type provided."]) elif not has_cred: - raise ValidationError([ - "You do not have permission to change account types."]) + raise ValidationError( + ["You do not have permission to change account types."] + ) elif T > request.user.AccountTypeID: # If the chosen account type is higher than the editor's account # type, the editor doesn't have permission to set the new type. - error = _("You do not have permission to change " - "this user's account type to %s.") % name + error = ( + _( + "You do not have permission to change " + "this user's account type to %s." + ) + % name + ) raise ValidationError([error]) - logger.debug(f"Trusted User '{request.user.Username}' has " - f"modified '{user.Username}' account's type to" - f" {name}.") + logger.debug( + f"Package Maintainer '{request.user.Username}' has " + f"modified '{user.Username}' account's type to" + f" {name}." + ) -def invalid_captcha(captcha_salt: str = None, captcha: str = None, - **kwargs) -> None: +def invalid_captcha(captcha_salt: str = None, captcha: str = None, **kwargs) -> None: if captcha_salt and captcha_salt not in get_captcha_salts(): raise ValidationError(["This CAPTCHA has expired. Please try again."]) diff --git a/aurweb/util.py b/aurweb/util.py index 6759794f..89efd852 100644 --- a/aurweb/util.py +++ b/aurweb/util.py @@ -1,39 +1,38 @@ import math import re import secrets +import shlex import string - from datetime import datetime -from distutils.util import strtobool as _strtobool +from hashlib import sha1 from http import HTTPStatus from subprocess import PIPE, Popen -from typing import Callable, Iterable, List, Tuple, Union +from typing import Callable, Iterable, Tuple, Union from urllib.parse import urlparse import fastapi import pygit2 - from email_validator import EmailSyntaxError, validate_email from fastapi.responses import JSONResponse +from sqlalchemy.orm import Query import aurweb.config +from aurweb import aur_logging, defaults -from aurweb import defaults, logging - -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) def make_random_string(length: int) -> str: alphanumerics = string.ascii_lowercase + string.digits - return ''.join([secrets.choice(alphanumerics) for i in range(length)]) + return "".join([secrets.choice(alphanumerics) for i in range(length)]) def make_nonce(length: int = 8): - """ Generate a single random nonce. Here, token_hex generates a hex + """Generate a single random nonce. Here, token_hex generates a hex string of 2 hex characters per byte, where the length give is nbytes. This means that to get our proper string length, we need to cut it in half and truncate off any remaining (in the case that - length was uneven). """ + length was uneven).""" return secrets.token_hex(math.ceil(length / 2))[:length] @@ -46,7 +45,7 @@ def valid_username(username): # Check that username contains: one or more alphanumeric # characters, an optional separator of '.', '-' or '_', followed # by alphanumeric characters. - return re.match(r'^[a-zA-Z0-9]+[.\-_]?[a-zA-Z0-9]+$', username) + return re.match(r"^[a-zA-Z0-9]+[.\-_]?[a-zA-Z0-9]+$", username) def valid_email(email): @@ -83,7 +82,7 @@ def valid_pgp_fingerprint(fp): def jsonify(obj): - """ Perform a conversion on obj if it's needed. """ + """Perform a conversion on obj if it's needed.""" if isinstance(obj, datetime): obj = int(obj.timestamp()) return obj @@ -99,24 +98,24 @@ def apply_all(iterable: Iterable, fn: Callable): return iterable -def sanitize_params(offset: str, per_page: str) -> Tuple[int, int]: +def sanitize_params(offset_str: str, per_page_str: str) -> Tuple[int, int]: try: - offset = int(offset) + offset = defaults.O if int(offset_str) < 0 else int(offset_str) except ValueError: offset = defaults.O try: - per_page = int(per_page) + per_page = defaults.PP if int(per_page_str) <= 0 else int(per_page_str) except ValueError: per_page = defaults.PP - return (offset, per_page) + return offset, per_page def strtobool(value: Union[str, bool]) -> bool: - if isinstance(value, str): - return _strtobool(value or "False") - return value + if not value: + return False + return str(value).lower() in ("y", "yes", "t", "true", "on", "1") def file_hash(filepath: str, hash_function: Callable) -> str: @@ -152,8 +151,7 @@ def git_search(repo: pygit2.Repository, commit_hash: str) -> int: return prefixlen -async def error_or_result(next: Callable, *args, **kwargs) \ - -> fastapi.Response: +async def error_or_result(next: Callable, *args, **kwargs) -> fastapi.Response: """ Try to return a response from `next`. @@ -175,9 +173,9 @@ async def error_or_result(next: Callable, *args, **kwargs) \ def parse_ssh_key(string: str) -> Tuple[str, str]: - """ Parse an SSH public key. """ + """Parse an SSH public key.""" invalid_exc = ValueError("The SSH public key is invalid.") - parts = re.sub(r'\s\s+', ' ', string.strip()).split() + parts = re.sub(r"\s\s+", " ", string.strip()).split() if len(parts) < 2: raise invalid_exc @@ -186,15 +184,35 @@ def parse_ssh_key(string: str) -> Tuple[str, str]: if prefix not in prefixes: raise invalid_exc - proc = Popen(["ssh-keygen", "-l", "-f", "-"], stdin=PIPE, stdout=PIPE, - stderr=PIPE) + proc = Popen(["ssh-keygen", "-l", "-f", "-"], stdin=PIPE, stdout=PIPE, stderr=PIPE) out, _ = proc.communicate(f"{prefix} {key}".encode()) if proc.returncode: raise invalid_exc - return (prefix, key) + return prefix, key -def parse_ssh_keys(string: str) -> List[Tuple[str, str]]: - """ Parse a list of SSH public keys. """ - return [parse_ssh_key(e) for e in string.splitlines()] +def parse_ssh_keys(string: str) -> set[Tuple[str, str]]: + """Parse a list of SSH public keys.""" + return set([parse_ssh_key(e) for e in string.strip().splitlines(True) if e.strip()]) + + +def shell_exec(cmdline: str, cwd: str) -> Tuple[int, str, str]: + args = shlex.split(cmdline) + proc = Popen(args, cwd=cwd, stdout=PIPE, stderr=PIPE) + out, err = proc.communicate() + return proc.returncode, out.decode().strip(), err.decode().strip() + + +def hash_query(query: Query): + return sha1( + str(query.statement.compile(compile_kwargs={"literal_binds": True})).encode() + ).hexdigest() + + +def get_client_ip(request: fastapi.Request) -> str: + """ + Returns the client's IP address for a Request. + Falls back to 'testclient' if request.client is None + """ + return request.client.host if request.client else "testclient" diff --git a/ci/tf/.terraform.lock.hcl b/ci/tf/.terraform.lock.hcl new file mode 100644 index 00000000..aa5501c4 --- /dev/null +++ b/ci/tf/.terraform.lock.hcl @@ -0,0 +1,61 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/dns" { + version = "3.3.2" + hashes = [ + "h1:HjskPLRqmCw8Q/kiSuzti3iJBSpcAvcBFdlwFFQuoDE=", + "zh:05d2d50e301318362a4a82e6b7a9734ace07bc01abaaa649c566baf98814755f", + "zh:1e9fd1c3bfdda777e83e42831dd45b7b9e794250a0f351e5fd39762e8a0fe15b", + "zh:40e715fc7a2ede21f919567249b613844692c2f8a64f93ee64e5b68bae7ac2a2", + "zh:454d7aa83000a6e2ba7a7bfde4bcf5d7ed36298b22d760995ca5738ab02ee468", + "zh:46124ded51b4153ad90f12b0305fdbe0c23261b9669aa58a94a31c9cca2f4b19", + "zh:55a4f13d20f73534515a6b05701abdbfc54f4e375ba25b2dffa12afdad20e49d", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7903b1ceb8211e2b8c79290e2e70906a4b88f4fba71c900eb3a425ce12f1716a", + "zh:b79fc4f444ef7a2fd7111a80428c070ad824f43a681699e99ab7f83074dfedbd", + "zh:ca9f45e0c4cb94e7d62536c226024afef3018b1de84f1ea4608b51bcd497a2a0", + "zh:ddc8bd894559d7d176e0ceb0bb1ae266519b01b315362ebfee8327bb7e7e5fa8", + "zh:e77334c0794ef8f9354b10e606040f6b0b67b373f5ff1db65bddcdd4569b428b", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.0.4" + hashes = [ + "h1:pe9vq86dZZKCm+8k1RhzARwENslF3SXb9ErHbQfgjXU=", + "zh:23671ed83e1fcf79745534841e10291bbf34046b27d6e68a5d0aab77206f4a55", + "zh:45292421211ffd9e8e3eb3655677700e3c5047f71d8f7650d2ce30242335f848", + "zh:59fedb519f4433c0fdb1d58b27c210b27415fddd0cd73c5312530b4309c088be", + "zh:5a8eec2409a9ff7cd0758a9d818c74bcba92a240e6c5e54b99df68fff312bbd5", + "zh:5e6a4b39f3171f53292ab88058a59e64825f2b842760a4869e64dc1dc093d1fe", + "zh:810547d0bf9311d21c81cc306126d3547e7bd3f194fc295836acf164b9f8424e", + "zh:824a5f3617624243bed0259d7dd37d76017097dc3193dac669be342b90b2ab48", + "zh:9361ccc7048be5dcbc2fafe2d8216939765b3160bd52734f7a9fd917a39ecbd8", + "zh:aa02ea625aaf672e649296bce7580f62d724268189fe9ad7c1b36bb0fa12fa60", + "zh:c71b4cd40d6ec7815dfeefd57d88bc592c0c42f5e5858dcc88245d371b4b8b1e", + "zh:dabcd52f36b43d250a3d71ad7abfa07b5622c69068d989e60b79b2bb4f220316", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hetznercloud/hcloud" { + version = "1.42.0" + hashes = [ + "h1:cr9lh26H3YbWSHb7OUnCoYw169cYO3Cjpt3yPnRhXS0=", + "zh:153b5f39d780e9a18bc1ea377d872647d328d943813cbd25d3d20863f8a37782", + "zh:35b9e95760c58cca756e34ad5f4138ac6126aa3e8c41b4a0f1d5dc9ee5666c73", + "zh:47a3cdbce982f2b4e17f73d4934bdb3e905a849b36fb59b80f87d852496ed049", + "zh:6a718c244c2ba300fbd43791661a061ad1ab16225ef3e8aeaa3db8c9eff12c85", + "zh:a2cbfc95c5e2c9422ed0a7b6292192c38241220d5b7813c678f937ab3ef962ae", + "zh:b837e118e08fd36aa8be48af7e9d0d3d112d2680c79cfc71cfe2501fb40dbefa", + "zh:bf66db8c680e18b77e16dc1f20ed1cdcc7876bfb7848c320ccb86f0fb80661ed", + "zh:c1ad80bbe48dc8a272a02dcdb4b12f019606f445606651c01e561b9d72d816b1", + "zh:d4e616701128ad14a6b5a427b0e9145ece4cad02aa3b5f9945c6d0b9ada8ab70", + "zh:d9d01f727037d028720100a5bc9fd213cb01e63e4b439a16f2f482c147976530", + "zh:dea047ee4d679370d4376fb746c4b959bf51dd06047c1c2656b32789c2433643", + "zh:e5ad7a3c556894bd40b28a874e7d2f6924876fa75fa443136a7d6ab9a00abbaa", + "zh:edf6e7e129157bd45e3da4a330d1ace17a336d417c3b77c620f302d440c368e8", + "zh:f610bc729866d58da9cffa4deae34dbfdba96655e855a87c6bb2cb7b35a8961c", + ] +} diff --git a/ci/tf/main.tf b/ci/tf/main.tf new file mode 100644 index 00000000..b149a621 --- /dev/null +++ b/ci/tf/main.tf @@ -0,0 +1,67 @@ +terraform { + backend "http" { + } +} + +provider "hcloud" { + token = var.hcloud_token +} + +provider "dns" { + update { + server = var.dns_server + key_name = var.dns_tsig_key + key_algorithm = var.dns_tsig_algorithm + key_secret = var.dns_tsig_secret + } +} + +resource "tls_private_key" "this" { + algorithm = "ED25519" +} + +resource "hcloud_ssh_key" "this" { + name = var.name + public_key = tls_private_key.this.public_key_openssh +} + +data "hcloud_image" "this" { + with_selector = "custom_image=archlinux" + most_recent = true + with_status = ["available"] +} + +resource "hcloud_server" "this" { + name = var.name + image = data.hcloud_image.this.id + server_type = var.server_type + datacenter = var.datacenter + ssh_keys = [hcloud_ssh_key.this.name] + + public_net { + ipv4_enabled = true + ipv6_enabled = true + } +} + +resource "hcloud_rdns" "this" { + for_each = { ipv4 : hcloud_server.this.ipv4_address, ipv6 : hcloud_server.this.ipv6_address } + + server_id = hcloud_server.this.id + ip_address = each.value + dns_ptr = "${var.name}.${var.dns_zone}" +} + +resource "dns_a_record_set" "this" { + zone = "${var.dns_zone}." + name = var.name + addresses = [hcloud_server.this.ipv4_address] + ttl = 300 +} + +resource "dns_aaaa_record_set" "this" { + zone = "${var.dns_zone}." + name = var.name + addresses = [hcloud_server.this.ipv6_address] + ttl = 300 +} diff --git a/ci/tf/terraform.tfvars b/ci/tf/terraform.tfvars new file mode 100644 index 00000000..14818592 --- /dev/null +++ b/ci/tf/terraform.tfvars @@ -0,0 +1,4 @@ +server_type = "cpx11" +datacenter = "fsn1-dc14" +dns_server = "redirect.archlinux.org" +dns_zone = "sandbox.archlinux.page" diff --git a/ci/tf/variables.tf b/ci/tf/variables.tf new file mode 100644 index 00000000..a4e710ee --- /dev/null +++ b/ci/tf/variables.tf @@ -0,0 +1,36 @@ +variable "hcloud_token" { + type = string + sensitive = true +} + +variable "dns_server" { + type = string +} + +variable "dns_tsig_key" { + type = string +} + +variable "dns_tsig_algorithm" { + type = string +} + +variable "dns_tsig_secret" { + type = string +} + +variable "dns_zone" { + type = string +} + +variable "name" { + type = string +} + +variable "server_type" { + type = string +} + +variable "datacenter" { + type = string +} diff --git a/ci/tf/versions.tf b/ci/tf/versions.tf new file mode 100644 index 00000000..2c72215a --- /dev/null +++ b/ci/tf/versions.tf @@ -0,0 +1,13 @@ +terraform { + required_providers { + tls = { + source = "hashicorp/tls" + } + hcloud = { + source = "hetznercloud/hcloud" + } + dns = { + source = "hashicorp/dns" + } + } +} diff --git a/cliff.toml b/cliff.toml index 12cd7e0b..3d3cb1c7 100644 --- a/cliff.toml +++ b/cliff.toml @@ -47,6 +47,6 @@ commit_parsers = [ # filter out the commits that are not matched by commit parsers filter_commits = false # glob pattern for matching git tags -tag_pattern = "*[0-9]*" +tag_pattern = "v[0-9]." # regex for skipping tags skip_tags = "v0.1.0-beta.1" diff --git a/conf/cgitrc.proto b/conf/cgitrc.proto index 1b3eacbd..ed53c51c 100644 --- a/conf/cgitrc.proto +++ b/conf/cgitrc.proto @@ -20,8 +20,8 @@ cache-static-ttl=60 root-title=AUR Package Repositories root-desc=Web interface to the AUR Package Repositories -header=/srv/http/aurweb/web/template/cgit/header.html -footer=/srv/http/aurweb/web/template/cgit/footer.html +header=/srv/http/aurweb/static/html/cgit/header.html +footer=/srv/http/aurweb/static/html/cgit/footer.html max-repodesc-length=50 max-blob-size=2048 max-stats=year diff --git a/conf/config.defaults b/conf/config.defaults index 722802cc..c9a6899f 100644 --- a/conf/config.defaults +++ b/conf/config.defaults @@ -14,8 +14,12 @@ passwd_min_len = 8 default_lang = en default_timezone = UTC sql_debug = 0 -login_timeout = 7200 +; 4 hours - default login_timeout +login_timeout = 14400 +; 30 days - default persistent_cookie_timeout persistent_cookie_timeout = 2592000 +; 400 days - default permanent_cookie_timeout +permanent_cookie_timeout = 34560000 max_filesize_uncompressed = 8388608 disable_http_login = 1 aur_location = https://aur.archlinux.org @@ -25,6 +29,7 @@ max_rpc_results = 5000 max_search_results = 2500 max_depends = 1000 aur_request_ml = aur-requests@lists.archlinux.org +ml_thread_url = https://lists.archlinux.org/archives/list/aur-requests@lists.archlinux.org/thread/%s request_idle_time = 1209600 request_archive_time = 15552000 auto_orphan_age = 15552000 @@ -37,15 +42,15 @@ enable-maintenance = 1 maintenance-exceptions = 127.0.0.1 render-comment-cmd = /usr/bin/aurweb-rendercomment localedir = /srv/http/aurweb/web/locale/ -; memcache, apc, or redis -; memcache/apc are supported in PHP, redis is supported in Python. +; cache: redis is supported in Python. cache = none cache_pkginfo_ttl = 86400 -memcache_servers = 127.0.0.1:11211 salt_rounds = 12 redis_address = redis://localhost ; Toggles traceback display in templates/errors/500.html. traceback = 0 +; Maximum number of characters for a comment +max_chars_comment = 5000 [ratelimit] request_limit = 4000 @@ -120,16 +125,28 @@ max-blob-size = 256000 [aurblup] db-path = /srv/http/aurweb/aurblup/ -sync-dbs = core extra community multilib testing community-testing +sync-dbs = core-testing extra-testing multilib-testing core extra multilib server = https://mirrors.kernel.org/archlinux/%s/os/x86_64 [mkpkglists] -archivedir = /srv/http/aurweb/web/html -packagesfile = /srv/http/aurweb/web/html/packages.gz -packagesmetafile = /srv/http/aurweb/web/html/packages-meta-v1.json.gz -packagesmetaextfile = /srv/http/aurweb/web/html/packages-meta-ext-v1.json.gz -pkgbasefile = /srv/http/aurweb/web/html/pkgbase.gz -userfile = /srv/http/aurweb/web/html/users.gz +archivedir = /srv/http/aurweb/archives +packagesfile = /srv/http/aurweb/archives/packages.gz +packagesmetafile = /srv/http/aurweb/archives/packages-meta-v1.json.gz +packagesmetaextfile = /srv/http/aurweb/archives/packages-meta-ext-v1.json.gz +pkgbasefile = /srv/http/aurweb/archives/pkgbase.gz +userfile = /srv/http/aurweb/archives/users.gz + +[git-archive] +author = git_archive.py +author-email = no-reply@archlinux.org + +; One week worth of seconds (86400 * 7) +popularity-interval = 604800 + +metadata-repo = /srv/http/aurweb/metadata.git +users-repo = /srv/http/aurweb/users.git +pkgbases-repo = /srv/http/aurweb/pkgbases.git +pkgnames-repo = /srv/http/aurweb/pkgnames.git [devel] ; commit_url is a format string used to produce a link to a commit hash. @@ -143,10 +160,23 @@ commit_url = https://gitlab.archlinux.org/archlinux/aurweb/-/commits/%s ; sed -r "s/^;?(commit_hash) =.*$/\1 = $(git rev-parse HEAD)/" config ;commit_hash = 1234567 -[tuvotereminder] -; Offsets used to determine when TUs should be reminded about +[votereminder] +; Offsets used to determine when Package Maintainers should be reminded about ; votes that they should make. -; Reminders will be sent out for all votes that a TU has not yet +; Reminders will be sent out for all votes that a Package Maintainer has not yet ; voted on based on `now + range_start <= End <= now + range_end`. range_start = 500 range_end = 172800 + +[cache] +; maximum number of keys/entries (for search results) in our redis cache, default is 50000 +max_search_entries = 50000 +; number of seconds after a cache entry for search queries expires, default is 10 minutes +expiry_time_search = 600 +; number of seconds after a cache entry for statistics queries expires, default is 5 minutes +expiry_time_statistics = 300 +; number of seconds after a cache entry for rss queries expires, default is 5 minutes +expiry_time_rss = 300 + +[tracing] +otlp_endpoint = http://localhost:4318/v1/traces diff --git a/conf/config.dev b/conf/config.dev index 923c34ff..716cafa2 100644 --- a/conf/config.dev +++ b/conf/config.dev @@ -6,7 +6,6 @@ ; development-specific options too. [database] -; PHP options: mysql, sqlite. ; FastAPI options: mysql. backend = mysql @@ -31,9 +30,6 @@ localedir = YOUR_AUR_ROOT/web/locale salt_rounds = 4 ; See config.defaults comment about cache. cache = none -; In docker, the memcached host is available. On a user's system, -; this should be set to localhost (most likely). -memcache_servers = memcached:11211 ; If cache = 'redis' this address is used to connect to Redis. redis_address = redis://127.0.0.1 aur_request_ml = aur-requests@localhost @@ -51,13 +47,6 @@ openid_configuration = http://127.0.0.1:8083/auth/realms/aurweb/.well-known/open client_id = aurweb client_secret = -[php] -; Address PHP should bind when spawned in development mode by aurweb.spawn. -bind_address = 127.0.0.1:8081 - -; Directory containing aurweb's PHP code, required by aurweb.spawn. -htmldir = YOUR_AUR_ROOT/web/html - [fastapi] ; Address uvicorn should bind when spawned in development mode by aurweb.spawn. bind_address = 127.0.0.1:8082 @@ -76,5 +65,14 @@ packagesmetaextfile = /var/lib/aurweb/archives/packages-meta-ext-v1.json.gz pkgbasefile = /var/lib/aurweb/archives/pkgbase.gz userfile = /var/lib/aurweb/archives/users.gz +[git-archive] +metadata-repo = metadata.git +users-repo = users.git +pkgbases-repo = pkgbases.git +pkgnames-repo = pkgnames.git + [aurblup] db-path = YOUR_AUR_ROOT/aurblup/ + +[tracing] +otlp_endpoint = http://tempo:4318/v1/traces diff --git a/doc/docker.md b/doc/docker.md index 22505f7a..c54184b8 100644 --- a/doc/docker.md +++ b/doc/docker.md @@ -65,12 +65,9 @@ Services | [mariadb](#mariadb) | 127.0.0.1:13306 | | [git](#git) | 127.0.0.1:2222 | | redis | 127.0.0.1:16379 | -| [php-fpm](#php-fpm) | 127.0.0.1:19000 | -| cgit-php | | | [fastapi](#fastapi) | 127.0.0.1:18000 | | cgit-fastapi | | | [nginx](#nginx) (fastapi) | 127.0.0.1:8444 | -| [nginx](#nginx) (php) | 127.0.0.1:8443 | There are more services which have not been referred to here; the services listed above encompass all notable services. Some @@ -113,16 +110,6 @@ to be used for the AUR. This service will perform setup in either case if the repository is not yet initialized. -#### php-fpm - -When running any services which use the _php-fpm_ backend or other -php-related services, users should define: - -- `AURWEB_PHP_PREFIX` - - Default: `https://localhost:8443` -- `AURWEB_SSHD_PREFIX` - - Default: `ssh://aur@localhost:2222` - #### fastapi The _fastapi_ service hosts a `gunicorn`, `uvicorn` or `hypercorn` @@ -145,20 +132,17 @@ backend or other fastapi-related services, users should define: #### nginx -The _nginx_ service binds to two host endpoints: 127.0.0.1:8444 (fastapi) -and 127.0.0.1:8443 (php). Each instance is available over the `https` +The _nginx_ service binds to host endpoint: 127.0.0.1:8444 (fastapi). +The instance is available over the `https` protocol as noted in the table below. | Impl | Host Binding | URL | |--------|----------------|------------------------| | Python | 127.0.0.1:8444 | https://localhost:8444 | -| PHP | 127.0.0.1:8443 | https://localhost:8443 | When running this service, the following variables should be defined: - `AURWEB_FASTAPI_PREFIX` - Default: `https://localhost:8444` -- `AURWEB_PHP_PREFIX` - - Default: `https://localhost:8443` - `AURWEB_SSHD_PREFIX` - Default: `ssh://aur@localhost:2222` diff --git a/doc/git-archive.md b/doc/git-archive.md new file mode 100644 index 00000000..d7c80f76 --- /dev/null +++ b/doc/git-archive.md @@ -0,0 +1,75 @@ +# aurweb Git Archive Specification + + + WARNING: This aurweb Git Archive implementation is + experimental and may be changed. + + +## Overview + +This git archive specification refers to the archive git repositories +created by [aurweb/scripts/git_archive.py](aurweb/scripts/git_archive.py) +using [spec modules](#spec-modules). + +## Configuration + +- `[git-archive]` + - `author` + - Git commit author + - `author-email` + - Git commit author email + +See an [official spec](#official-specs)'s documentation for spec-specific +configurations. + +## Fetch/Update Archives + +When a client has not yet fetched any initial archives, they should +shallow-clone the repository: + + $ git clone --depth=1 https://aur.archlinux.org/archive.git aurweb-archive + +When updating, the repository is already cloned and changes need to be pulled +from remote: + + # To update: + $ cd aurweb-archive && git pull + +For end-user production applications, see +[Minimize Disk Space](#minimize-disk-space). + +## Minimize Disk Space + +Using `git gc` on the repository will compress revisions and remove +unreachable objects which grow the repository a considerable amount +each commit. It is recommended that the following command is used +after cloning the archive or pulling updates: + + $ cd aurweb-archive && git gc --aggressive + +## Spec Modules + +Each aurweb spec module belongs to the `aurweb.archives.spec` package. For +example: a spec named "example" would be located at +`aurweb.archives.spec.example`. + +[Official spec listings](#official-specs) use the following format: + +- `spec_name` + - Spec description; what this spec produces + - `` + +### Official Specs + +- [metadata](doc/specs/metadata.md) + - Package RPC `type=info` metadata + - [metadata-repo](repos/metadata-repo.md) +- [users](doc/specs/users.md) + - List of users found in the database + - [users-repo](repos/users-repo.md) +- [pkgbases](doc/specs/pkgbases.md) + - List of package bases found in the database + - [pkgbases-repo](repos/pkgbases-repo.md) +- [pkgnames](doc/specs/pkgnames.md) + - List of package names found in the database + - [pkgnames-repo](repos/pkgnames-repo.md) diff --git a/doc/git-interface.txt b/doc/git-interface.txt index 8c6806f7..39c2b487 100644 --- a/doc/git-interface.txt +++ b/doc/git-interface.txt @@ -35,7 +35,7 @@ usually points to the git-serve program. If SSH has been configured to pass on the AUR_OVERWRITE environment variable (via SendEnv, see ssh_config(5) for details) and the user's account is a -registered Trusted User or Developer, this will be passed on to the git-update +registered Package Maintainer or Developer, this will be passed on to the git-update program in order to enable a non-fast-forward push. The INSTALL file in the top-level directory contains detailed instructions on @@ -53,7 +53,6 @@ The git-serve command, the "aurweb shell", provides different subcommands: * The restore command can be used to restore a deleted package base. * The set-comaintainers command modifies the co-maintainers of a package base. * The set-keywords command modifies the keywords assigned to a package base. -* The setup-repo command can be used to create a new repository. * The vote/unvote command can be used to vote/unvote for a package base. * The git-{receive,upload}-pack commands are redirected to git-shell(1). @@ -71,8 +70,8 @@ The Update Hook: git-update The Git update hook, called git-update, performs several subtasks: * Prevent from creating branches or tags other than master. -* Deny non-fast-forwards, except for Trusted Users and Developers. -* Deny blacklisted packages, except for Trusted Users and Developers. +* Deny non-fast-forwards, except for Package Maintainers and Developers. +* Deny blacklisted packages, except for Package Maintainers and Developers. * Verify each new commit (validate meta data, impose file size limits, ...) * Update package base information and package information in the database. * Update the named branch and the namespaced HEAD ref of the package. @@ -110,7 +109,7 @@ is also recommended to disable automatic garbage collection by setting receive.autogc to false. Remember to periodically run `git gc` manually or setup a maintenance script which initiates the garbage collection if you follow this advice. For gc.pruneExpire, we recommend "3.months.ago", such that commits -that became unreachable by TU intervention are kept for a while. +that became unreachable by Package Maintainer intervention are kept for a while. Script Wrappers (poetry) ------------------------ diff --git a/doc/i18n.txt b/doc/i18n.md similarity index 69% rename from doc/i18n.txt rename to doc/i18n.md index 44fb0f1f..d81c467d 100644 --- a/doc/i18n.txt +++ b/doc/i18n.md @@ -3,9 +3,9 @@ aurweb Translation This document describes how to create and maintain aurweb translations. -Creating an aurweb translation requires a Transifex (http://www.transifex.com/) +Creating an aurweb translation requires a Transifex (https://app.transifex.com/) account. You will need to register with a translation team on the aurweb -project page (http://www.transifex.com/projects/p/aurweb/). +project page (https://app.transifex.com/lfleischer/aurweb/). Creating a New Translation @@ -21,23 +21,23 @@ strings for the translation to be usable, and it may have to be disabled. 1. Check out the aurweb source using git: -$ git clone https://gitlab.archlinux.org/archlinux/aurweb.git aurweb-git + $ git clone https://gitlab.archlinux.org/archlinux/aurweb.git aurweb-git -2. Go into the "po/" directory in the aurweb source and run msginit(1) to +2. Go into the "po/" directory in the aurweb source and run [msginit(1)][msginit] to create a initial translation file from our translation catalog: -$ cd aurweb-git -$ git checkout master -$ git pull -$ cd po -$ msginit -l -o .po -i aurweb.pot + $ cd aurweb-git + $ git checkout master + $ git pull + $ cd po + $ msginit -l -o .po -i aurweb.pot 3. Use some editor or a translation helper like poedit to add translations: -$ poedit .po + $ poedit .po 5. If you have a working aurweb setup, add a line for the new translation in - "web/lib/config.inc.php.proto" and test if everything looks right. + "po/Makefile" and test if everything looks right. 6. Upload the newly created ".po" file to Transifex. If you don't like the web interface, you can also use transifex-client to do that (see below). @@ -49,13 +49,15 @@ Updating an Existing Translation 1. Download current translation files from Transifex. You can also do this using transifex-client which is available through the AUR: -$ tx pull -a + $ tx pull -a 2. Update the existing translation file using an editor or a tool like poedit: -$ poedit po/.po + $ poedit po/.po 3. Push the updated translation file back to Transifex. Using transifex-client, this works as follows: -$ tx push -r aurweb.aurwebpot -t -l + $ tx push -r aurweb.aurwebpot -t -l + +[msginit]: https://man.archlinux.org/man/msginit.1 diff --git a/doc/maintenance.txt b/doc/maintenance.txt index c52cf76f..68766402 100644 --- a/doc/maintenance.txt +++ b/doc/maintenance.txt @@ -12,8 +12,8 @@ package maintenance from the command-line. More details can be found in The web interface can be used to browse packages, view package details, manage aurweb accounts, add comments, vote for packages, flag packages, and submit -requests. Trusted Users can update package maintainers and delete/merge -packages. The web interface also includes an area for Trusted Users to post +requests. Package Maintainers can update package maintainers and delete/merge +packages. The web interface also includes an area for Package Maintainers to post AUR-related proposals and vote on them. The RPC interface can be used to query package information via HTTP. @@ -21,7 +21,7 @@ The RPC interface can be used to query package information via HTTP. Installation ------------ -The web backend requires a web server with PHP and an SQL database. The Git/SSH +The web backend requires a web server and an SQL database. The Git/SSH interface requires Python, several Python modules and an up-to-date version of Git. APCu or memcached can be used to reduce load on the database server. @@ -62,39 +62,58 @@ computations and clean up the database: the official repositories. It is also used to prevent users from uploading packages that are in the official repositories already. -* aurweb-tuvotereminder sends out reminders to TUs if the voting period for a - TU proposal ends soon. +* aurweb-votereminder sends out reminders if the voting period for a + Package Maintainer proposal ends soon. * aurweb-popupdate is used to recompute the popularity score of packages. * aurweb-pkgmaint automatically removes empty repositories that were created within the last 24 hours but never populated. -* aurweb-mkpkglists generates the package list files; it takes an optional - --extended flag, which additionally produces multiinfo metadata. It also - generates {archive.gz}.sha256 files that should be located within +* [Deprecated] aurweb-mkpkglists generates the package list files; it takes + an optional --extended flag, which additionally produces multiinfo metadata. + It also generates {archive.gz}.sha256 files that should be located within mkpkglists.archivedir which contain a SHA-256 hash of their matching .gz counterpart. * aurweb-usermaint removes the last login IP address of all users that did not login within the past seven days. +* aurweb-git-archive generates Git repository archives based on a --spec. + This script is a new generation of aurweb-mkpkglists, which creates and + maintains Git repository versions of the archives produced by + aurweb-mkpkglists. See doc/git-archive.md for detailed documentation. + These scripts can be installed by running `poetry install` and are usually scheduled using Cron. The current setup is: ---- -*/5 * * * * poetry run aurweb-mkpkglists [--extended] +# Run aurweb-git-archive --spec metadata directly after +# aurweb-mkpkglists so that they are executed sequentially, since +# both scripts are quite heavy. `aurweb-mkpkglists` should be removed +# from here once its deprecation period has ended. +*/5 * * * * poetry run aurweb-mkpkglists [--extended] && poetry run aurweb-git-archive --spec metadata + +# Usernames +*/5 * * * * poetry run aurweb-git-archive --spec users + +# Package base names +*/5 * * * * poetry run aurweb-git-archive --spec pkgbases + +# Package names +*/5 * * * * poetry run aurweb-git-archive --spec pkgnames + 1 */2 * * * poetry run aurweb-popupdate 2 */2 * * * poetry run aurweb-aurblup 3 */2 * * * poetry run aurweb-pkgmaint 4 */2 * * * poetry run aurweb-usermaint -5 */12 * * * poetry run aurweb-tuvotereminder +5 */12 * * * poetry run aurweb-votereminder ---- Advanced Administrative Features -------------------------------- -Trusted Users can set the AUR_OVERWRITE environment variable to enable +Package Maintainers can set the AUR_OVERWRITE environment variable to enable non-fast-forward pushes to the Git repositories. This feature is documented in `doc/git-interface.txt`. diff --git a/doc/repos/metadata-repo.md b/doc/repos/metadata-repo.md new file mode 100644 index 00000000..cc678f40 --- /dev/null +++ b/doc/repos/metadata-repo.md @@ -0,0 +1,121 @@ +# Repository: metadata-repo + +## Overview + +The resulting repository contains RPC `type=info` JSON data for packages, +split into two different files: + +- `pkgbase.json` contains details about each package base in the AUR +- `pkgname.json` contains details about each package in the AUR + +See [Data](#data) for a breakdown of how data is presented in this +repository based off of a RPC `type=info` base. + +See [File Layout](#file-layout) for a detailed summary of the layout +of these files and the data contained within. + +**NOTE: `Popularity` now requires a client-side calculation, see [Popularity Calculation](#popularity-calculation).** + +## Data + +This repository contains RPC `type=info` data for all packages found +in AUR's database, reorganized to be suitable for Git repository +changes. + +- `pkgname.json` holds Package-specific metadata + - Some fields have been removed from `pkgname.json` objects + - `ID` + - `PackageBaseID -> ID` (moved to `pkgbase.json`) + - `NumVotes` (moved to `pkgbase.json`) + - `Popularity` (moved to `pkgbase.json`) +- `pkgbase.json` holds PackageBase-specific metadata + - Package Base fields from `pkgname.json` have been moved over to + `pkgbase.json` + - `ID` + - `Keywords` + - `FirstSubmitted` + - `LastModified` + - `OutOfDate` + - `Maintainer` + - `URLPath` + - `NumVotes` + - `Popularity` + - `PopularityUpdated` + +## Popularity Calculation + +Clients intending to use popularity data from this archive **must** +perform a decay calculation on their end to reflect a close approximation +of up-to-date popularity. + +Putting this step onto the client allows the server to maintain +less popularity record updates, dramatically improving archiving +of popularity data. The same calculation is done on the server-side +when producing outputs for RPC `type=info` and package pages. + +``` +Let T = Current UTC timestamp in seconds +Let PU = PopularityUpdated timestamp in seconds + +# The delta between now and PU in days +Let D = (T - PU) / 86400 + +# Calculate up-to-date popularity: +P = Popularity * (0.98^D) +``` + +We can see that the resulting up-to-date popularity value decays as +the exponent is increased: +- `1.0 * (0.98^1) = 0.98` +- `1.0 * (0.98^2) = 0.96039999` +- ... + +This decay calculation is essentially pushing back the date found for +votes by the exponent, which takes into account the time-factor. However, +since this calculation is based off of decimals and exponents, it +eventually becomes imprecise. The AUR updates these records on a forced +interval and whenever a vote is added to or removed from a particular package +to avoid imprecision from being an issue for clients + +## File Layout + +#### pkgbase.json: + + { + "pkgbase1": { + "FirstSubmitted": 123456, + "ID": 1, + "LastModified": 123456, + "Maintainer": "kevr", + "OutOfDate": null, + "URLPath": "/cgit/aur.git/snapshot/pkgbase1.tar.gz", + "NumVotes": 1, + "Popularity": 1.0, + "PopularityUpdated": 12345567753.0 + }, + ... + } + +#### pkgname.json: + + { + "pkg1": { + "CheckDepends": [], # Only included if a check dependency exists + "Conflicts": [], # Only included if a conflict exists + "Depends": [], # Only included if a dependency exists + "Description": "some description", + "Groups": [], # Only included if a group exists + "ID": 1, + "Keywords": [], + "License": [], + "MakeDepends": [], # Only included if a make dependency exists + "Name": "pkg1", + "OptDepends": [], # Only included if an opt dependency exists + "PackageBase": "pkgbase1", + "Provides": [], # Only included if `provides` is defined + "Replaces": [], # Only included if `replaces` is defined + "URL": "https://some_url.com", + "Version": "1.0-1" + }, + ... + } diff --git a/doc/repos/pkgbases-repo.md b/doc/repos/pkgbases-repo.md new file mode 100644 index 00000000..f4cb896f --- /dev/null +++ b/doc/repos/pkgbases-repo.md @@ -0,0 +1,15 @@ +# Repository: pkgbases-repo + +## Overview + +- `pkgbase.json` contains a list of package base names + +## File Layout + +### pkgbase.json: + + [ + "pkgbase1", + "pkgbase2", + ... + ] diff --git a/doc/repos/pkgnames-repo.md b/doc/repos/pkgnames-repo.md new file mode 100644 index 00000000..ae6fb4ed --- /dev/null +++ b/doc/repos/pkgnames-repo.md @@ -0,0 +1,15 @@ +# Repository: pkgnames-repo + +## Overview + +- `pkgname.json` contains a list of package names + +## File Layout + +### pkgname.json: + + [ + "pkgname1", + "pkgname2", + ... + ] diff --git a/doc/repos/users-repo.md b/doc/repos/users-repo.md new file mode 100644 index 00000000..23db9cfb --- /dev/null +++ b/doc/repos/users-repo.md @@ -0,0 +1,15 @@ +# Repository: users-repo + +## Overview + +- `users.json` contains a list of usernames + +## File Layout + +### users.json: + + [ + "user1", + "user2", + ... + ] diff --git a/doc/specs/metadata.md b/doc/specs/metadata.md new file mode 100644 index 00000000..282c0dd5 --- /dev/null +++ b/doc/specs/metadata.md @@ -0,0 +1,14 @@ +# Git Archive Spec: metadata + +## Configuration + +- `[git-archive]` + - `metadata-repo` + - Path to package metadata git repository location + +## Repositories + +For documentation on each one of these repositories, follow their link, +which brings you to a topical markdown for that repository. + +- [metadata-repo](doc/repos/metadata-repo.md) diff --git a/doc/specs/pkgbases.md b/doc/specs/pkgbases.md new file mode 100644 index 00000000..80279070 --- /dev/null +++ b/doc/specs/pkgbases.md @@ -0,0 +1,14 @@ +# Git Archive Spec: pkgbases + +## Configuration + +- `[git-archive]` + - `pkgbases-repo` + - Path to pkgbases git repository location + +## Repositories + +For documentation on each one of these repositories, follow their link, +which brings you to a topical markdown for that repository. + +- [pkgbases-repo](doc/repos/pkgbases-repo.md) diff --git a/doc/specs/pkgnames.md b/doc/specs/pkgnames.md new file mode 100644 index 00000000..0a4a907d --- /dev/null +++ b/doc/specs/pkgnames.md @@ -0,0 +1,14 @@ +# Git Archive Spec: pkgnames + +## Configuration + +- `[git-archive]` + - `pkgnames-repo` + - Path to pkgnames git repository location + +## Repositories + +For documentation on each one of these repositories, follow their link, +which brings you to a topical markdown for that repository. + +- [pkgnames-repo](doc/repos/pkgnames-repo.md) diff --git a/doc/specs/popularity.md b/doc/specs/popularity.md new file mode 100644 index 00000000..3084f458 --- /dev/null +++ b/doc/specs/popularity.md @@ -0,0 +1,14 @@ +# Git Archive Spec: popularity + +## Configuration + +- `[git-archive]` + - `popularity-repo` + - Path to popularity git repository location + +## Repositories + +For documentation on each one of these repositories, follow their link, +which brings you to a topical markdown for that repository. + +- [popularity-repo](doc/repos/popularity-repo.md) diff --git a/doc/specs/users.md b/doc/specs/users.md new file mode 100644 index 00000000..25396154 --- /dev/null +++ b/doc/specs/users.md @@ -0,0 +1,14 @@ +# Git Archive Spec: users + +## Configuration + +- `[git-archive]` + - `users-repo` + - Path to users git repository location + +## Repositories + +For documentation on each one of these repositories, follow their link, +which brings you to a topical markdown for that repository. + +- [users-repo](doc/repos/users-repo.md) diff --git a/doc/web-auth.md b/doc/web-auth.md new file mode 100644 index 00000000..c8604fed --- /dev/null +++ b/doc/web-auth.md @@ -0,0 +1,104 @@ +# aurweb Web Authentication + +aurweb uses an HTTP cookie to persist user sessions across requests. +This cookie **must** be delivered with a request in order to be considered +an authenticated user. + +See [HTTP Cookie](#http-cookie) for detailed information about the cookie. + +## HTTP Cookie + +aurweb utilizes an HTTP cookie by the name of `AURSID` to track +user authentication across requests. + +This cookie's requirements changes due to aurweb's configuration +in the following ways: + +- `options.disable_http_login: 0` + - [Samesite=LAX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#samesite_attribute), Max-Age +- `options.disable_http_login: 1` + - [Samesite=LAX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#samesite_attribute), [Secure, HttpOnly](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies), Max-Age + +### Max-Age + +The value used for the `AURSID` Max-Age attribute is decided based +off of the "Remember Me" checkbox on the login page. If it was not +checked, we don't set Max-Age and it becomes a session cookie. +Otherwise we make it a persistent cookie and for the expiry date +we use `options.persistent_cookie_timeout`. +It indicates the number of seconds the session should live. + +### Notes + +At all times, aur.archlinux.org operates over HTTPS. Secure cookies will +only remain intact when subsequently requesting an aurweb route through +the HTTPS scheme at the same host as the cookie was obtained. + +## Login Process + +When a user logs in to aurweb, the following steps are taken: + +1. Was a Referer header delivered from an address starting with +`{aurweb_url}/login`? + 1. No, an HTTP 400 Bad Request response is returned + 2. Yes, move on to 2 +2. Does a Users database record exist for the given username/email? + 1. No, you are returned to the login page with `Bad username or password.` + error + 2. Yes, move on to 3 +3. Is the user suspended? + 1. Yes, you are returned to the login page with `Account Suspended` error + 2. No, move on to 4 +4. Can the user login with the given password? + 1. No, you are returned to the login page with `Bad username or password.` + error + 2. Yes, move on to 5 +5. Update the user's `LastLogin` and `LastLoginIPAddress` columns +6. Does the user have a related Sessions record? + 1. No, generate a new Sessions record with a new unique `SessionID` + 2. Yes, update the Sessions record's `SessionID` column with a new unique + string and update the Sessions record's `LastUpdateTS` column if it has + expired + 3. In both cases, set the user's `InactivityTS` column to `0` + 4. In both cases, return the new `SessionID` column value and move on to 7 +7. Return a redirect to the `next` GET variable with the +following cookies set: + 1. `AURSID` + - Unique session string matching the user's related + `Sessions.SessionID` column + 2. `AURTZ` + - User's timezone setting + 3. `AURLANG` + - User's language setting + 4. `AURREMEMBER` + - Boolean state of the "Remember Me" checkbox when login submitted + +## Auth Verification + +When a request is made toward aurweb, a middleware is responsible for +verifying the user's auth cookie. If no valid `AURSID` cookie could be +found for a user in the database, the request is considered unauthenticated. + +The following list of steps describes exactly how this verification works: +1. Was the `AURSID` cookie delivered? + 1. No, the algorithm ends, you are considered unauthenticated + 2. Yes, move on to 2 +2. Was the `AURREMEMBER` cookie delivered with a value of `True`? + 1. No, set the expected session timeout **T** to `options.login_timeout` + 2. Yes, set the expected session timeout **T** to + `options.persistent_cookie_timeout` +3. Does a Sessions database record exist which matches the `AURSID`? + 1. No, the algorithm ends, you are considered unauthenticated + 2. Yes, move on to 4 +4. Does the Sessions record's LastUpdateTS column fit within `utcnow - T`? + 1. No, the Sessions record at hand is deleted, the algorithm ends, you + are considered unauthenticated + 2. Yes, move on to 5 +5. You are considered authenticated + +## aur.archlinux.org Auth-Related Configuration + +- Operates over HTTPS with a Let's Encrypt SSL certificate +- `options.disable_http_login: 1` +- `options.login_timeout: ` +- `options.persistent_cookie_timeout: ` diff --git a/docker-compose.aur-dev.yml b/docker-compose.aur-dev.yml index 0b91dd93..265ba6db 100644 --- a/docker-compose.aur-dev.yml +++ b/docker-compose.aur-dev.yml @@ -1,14 +1,10 @@ -version: "3.8" - +--- services: ca: volumes: - data:/data - step:/root/.step - memcached: - restart: always - redis: restart: always @@ -32,11 +28,6 @@ services: - data:/data - smartgit_run:/var/run/smartgit - cgit-php: - restart: always - volumes: - - ${GIT_DATA_DIR}:/aurweb/aur.git - cgit-fastapi: restart: always volumes: @@ -48,14 +39,6 @@ services: - mariadb_run:/var/run/mysqld - archives:/var/lib/aurweb/archives - php-fpm: - restart: always - environment: - - AURWEB_PHP_PREFIX=${AURWEB_PHP_PREFIX} - - AURWEB_SSHD_PREFIX=${AURWEB_SSHD_PREFIX} - volumes: - - data:/data - fastapi: restart: always environment: diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 1e466730..b0961521 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -1,16 +1,10 @@ -version: "3.8" - +--- services: ca: volumes: - ./data:/data - step:/root/.step - mariadb_init: - depends_on: - mariadb: - condition: service_healthy - git: volumes: - git_data:/aurweb/aur.git @@ -21,20 +15,6 @@ services: - git_data:/aurweb/aur.git - ./data:/data - smartgit_run:/var/run/smartgit - depends_on: - mariadb: - condition: service_healthy - - php-fpm: - volumes: - - ./data:/data - - ./aurweb:/aurweb/aurweb - - ./migrations:/aurweb/migrations - - ./test:/aurweb/test - - ./web/html:/aurweb/web/html - - ./web/template:/aurweb/web/template - - ./web/lib:/aurweb/web/lib - - ./templates:/aurweb/templates fastapi: volumes: @@ -42,9 +22,6 @@ services: - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations - ./test:/aurweb/test - - ./web/html:/aurweb/web/html - - ./web/template:/aurweb/web/template - - ./web/lib:/aurweb/web/lib - ./templates:/aurweb/templates nginx: diff --git a/docker-compose.yml b/docker-compose.yml index a56cbe72..ad578523 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,4 @@ +--- # # Docker service definitions for the aurweb project. # @@ -10,16 +11,12 @@ # - `ca` - Certificate Authority generation # - `git` - `port 2222` - Git over SSH server # - `fastapi` - hypercorn service for aurweb's FastAPI app -# - `php-fpm` - Execution server for PHP aurweb -# - `nginx` - `ports 8444 (FastAPI), 8443 (PHP)` - Everything -# - You can reach `nginx` via FastAPI at `https://localhost:8444/` -# or via PHP at `https://localhost:8443/`. CGit can be reached -# via the `/cgit/` request uri on either server. +# - `nginx` - `port 8444 (FastAPI) +# - You can reach `nginx` via FastAPI at `https://localhost:8444/`. +# CGit can be reached via the `/cgit/` request uri on either server. # # Copyright (C) 2021 aurweb Development # All Rights Reserved. -version: "3.8" - services: aurweb-image: build: . @@ -31,17 +28,11 @@ services: entrypoint: /docker/ca-entrypoint.sh command: /docker/scripts/run-ca.sh healthcheck: - test: "bash /docker/health/run-ca.sh" - interval: 2s + test: "bash /docker/health/ca.sh" + interval: 3s + volumes: + - step:/root/.step - memcached: - image: aurweb:latest - init: true - command: /docker/scripts/run-memcached.sh - healthcheck: - test: "bash /docker/health/memcached.sh" - interval: 2s - redis: image: aurweb:latest init: true @@ -49,7 +40,7 @@ services: command: /docker/scripts/run-redis.sh healthcheck: test: "bash /docker/health/redis.sh" - interval: 2s + interval: 3s ports: - "127.0.0.1:16379:6379" @@ -57,7 +48,7 @@ services: image: aurweb:latest init: true entrypoint: /docker/mariadb-entrypoint.sh - command: /usr/bin/mysqld_safe --datadir=/var/lib/mysql + command: /usr/bin/mariadbd-safe --datadir=/var/lib/mysql ports: # This will expose mariadbd on 127.0.0.1:13306 in the host. # Ex: `mysql -uaur -paur -h 127.0.0.1 -P 13306 aurweb` @@ -67,7 +58,7 @@ services: - mariadb_data:/var/lib/mysql healthcheck: test: "bash /docker/health/mariadb.sh" - interval: 2s + interval: 3s mariadb_init: image: aurweb:latest @@ -89,7 +80,7 @@ services: environment: - MARIADB_PRIVILEGED=1 entrypoint: /docker/mariadb-entrypoint.sh - command: /usr/bin/mysqld_safe --datadir=/var/lib/mysql + command: /usr/bin/mariadbd-safe --datadir=/var/lib/mysql ports: # This will expose mariadbd on 127.0.0.1:13307 in the host. # Ex: `mysql -uaur -paur -h 127.0.0.1 -P 13306 aurweb` @@ -98,7 +89,7 @@ services: - mariadb_test_run:/var/run/mysqld # Bind socket in this volume. healthcheck: test: "bash /docker/health/mariadb.sh" - interval: 2s + interval: 3s git: image: aurweb:latest @@ -113,10 +104,12 @@ services: - "2222:2222" healthcheck: test: "bash /docker/health/sshd.sh" - interval: 2s + interval: 3s depends_on: + mariadb: + condition: service_healthy mariadb_init: - condition: service_started + condition: service_completed_successfully volumes: - mariadb_run:/var/run/mysqld @@ -129,27 +122,10 @@ services: command: /docker/scripts/run-smartgit.sh healthcheck: test: "bash /docker/health/smartgit.sh" - interval: 2s - - cgit-php: - image: aurweb:latest - init: true - environment: - - AUR_CONFIG=/aurweb/conf/config - - CGIT_CLONE_PREFIX=${AURWEB_PHP_PREFIX} - - CGIT_CSS=/css/cgit.css - entrypoint: /docker/cgit-entrypoint.sh - command: /docker/scripts/run-cgit.sh 3000 - healthcheck: - test: "bash /docker/health/cgit.sh 3000" - interval: 2s + interval: 3s depends_on: - git: + mariadb: condition: service_healthy - ports: - - "127.0.0.1:13000:3000" - volumes: - - git_data:/aurweb/aur.git cgit-fastapi: image: aurweb:latest @@ -162,7 +138,7 @@ services: command: /docker/scripts/run-cgit.sh 3000 healthcheck: test: "bash /docker/health/cgit.sh 3000" - interval: 2s + interval: 3s depends_on: git: condition: service_healthy @@ -180,39 +156,15 @@ services: entrypoint: /docker/cron-entrypoint.sh command: /docker/scripts/run-cron.sh depends_on: + mariadb: + condition: service_healthy mariadb_init: - condition: service_started + condition: service_completed_successfully volumes: - ./aurweb:/aurweb/aurweb - mariadb_run:/var/run/mysqld - archives:/var/lib/aurweb/archives - php-fpm: - image: aurweb:latest - init: true - environment: - - AUR_CONFIG=/aurweb/conf/config - - AURWEB_PHP_PREFIX=${AURWEB_PHP_PREFIX} - - AURWEB_SSHD_PREFIX=${AURWEB_SSHD_PREFIX} - - AUR_CONFIG_IMMUTABLE=${AUR_CONFIG_IMMUTABLE:-0} - entrypoint: /docker/php-entrypoint.sh - command: /docker/scripts/run-php.sh - healthcheck: - test: "bash /docker/health/php.sh" - interval: 2s - depends_on: - git: - condition: service_healthy - memcached: - condition: service_healthy - cron: - condition: service_started - volumes: - - mariadb_run:/var/run/mysqld - - archives:/var/lib/aurweb/archives - ports: - - "127.0.0.1:19000:9000" - fastapi: image: aurweb:latest init: true @@ -228,7 +180,7 @@ services: command: /docker/scripts/run-fastapi.sh "${FASTAPI_BACKEND}" healthcheck: test: "bash /docker/health/fastapi.sh ${FASTAPI_BACKEND}" - interval: 2s + interval: 3s depends_on: git: condition: service_healthy @@ -236,6 +188,12 @@ services: condition: service_healthy cron: condition: service_started + mariadb: + condition: service_healthy + mariadb_init: + condition: service_completed_successfully + tempo: + condition: service_healthy volumes: - archives:/var/lib/aurweb/archives - mariadb_run:/var/run/mysqld @@ -250,15 +208,12 @@ services: entrypoint: /docker/nginx-entrypoint.sh command: /docker/scripts/run-nginx.sh ports: - - "127.0.0.1:8443:8443" # PHP - "127.0.0.1:8444:8444" # FastAPI healthcheck: test: "bash /docker/health/nginx.sh" - interval: 2s + interval: 3s depends_on: ca: - condition: service_started - cgit-php: condition: service_healthy cgit-fastapi: condition: service_healthy @@ -266,8 +221,6 @@ services: condition: service_healthy fastapi: condition: service_healthy - php-fpm: - condition: service_healthy sharness: image: aurweb:latest @@ -288,9 +241,6 @@ services: - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations - ./test:/aurweb/test - - ./web/html:/aurweb/web/html - - ./web/template:/aurweb/web/template - - ./web/lib:/aurweb/web/lib - ./templates:/aurweb/templates pytest-mysql: @@ -317,9 +267,6 @@ services: - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations - ./test:/aurweb/test - - ./web/html:/aurweb/web/html - - ./web/template:/aurweb/web/template - - ./web/lib:/aurweb/web/lib - ./templates:/aurweb/templates test: @@ -344,11 +291,58 @@ services: - ./aurweb:/aurweb/aurweb - ./migrations:/aurweb/migrations - ./test:/aurweb/test - - ./web/html:/aurweb/web/html - - ./web/template:/aurweb/web/template - - ./web/lib:/aurweb/web/lib - ./templates:/aurweb/templates + grafana: + # TODO: check if we need init: true + image: grafana/grafana:11.1.3 + environment: + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_DISABLE_LOGIN_FORM=true + - GF_LOG_LEVEL=warn + # check if depends ar ecorrect, does stopping or restarting a child exit grafana? + depends_on: + prometheus: + condition: service_healthy + tempo: + condition: service_healthy + ports: + - "127.0.0.1:3000:3000" + volumes: + - ./docker/config/grafana/datasources:/etc/grafana/provisioning/datasources + + prometheus: + image: prom/prometheus:latest + command: + - --config.file=/etc/prometheus/prometheus.yml + - --web.enable-remote-write-receiver + - --web.listen-address=prometheus:9090 + healthcheck: + # TODO: check if there is a status route + test: "sh /docker/health/prometheus.sh" + interval: 3s + ports: + - "127.0.0.1:9090:9090" + volumes: + - ./docker/config/prometheus.yml:/etc/prometheus/prometheus.yml + - ./docker/health/prometheus.sh:/docker/health/prometheus.sh + + tempo: + image: grafana/tempo:2.5.0 + command: + - -config.file=/etc/tempo/config.yml + healthcheck: + # TODO: check if there is a status route + test: "sh /docker/health/tempo.sh" + interval: 3s + ports: + - "127.0.0.1:3200:3200" + - "127.0.0.1:4318:4318" + volumes: + - ./docker/config/tempo.yml:/etc/tempo/config.yml + - ./docker/health/tempo.sh:/docker/health/tempo.sh + volumes: mariadb_test_run: {} mariadb_run: {} # Share /var/run/mysqld/mysqld.sock diff --git a/docker/README.md b/docker/README.md index 89dbb739..e473582b 100644 --- a/docker/README.md +++ b/docker/README.md @@ -41,13 +41,13 @@ docker compose run test ### Generating Dummy Data Before you can make meaningful queries to the cluster, it needs some data. -Luckily such data can be generated. First, `docker ps` to discover the ID of the -container running the FastAPI. Then: +Luckily such data can be generated. ```sh -docker exec -it /bin/bash -./scheme/gendummydata.py dummy.sql -mysql aurweb < dummy.sql +docker compose exec fastapi /bin/bash +pacman -S words fortune-mod +./schema/gendummydata.py dummy.sql +mariadb aurweb < dummy.sql ``` The generation script may prompt you to install other Arch packages before it @@ -55,8 +55,7 @@ can proceed. ### Querying the RPC -The Fast (Python) API runs on Port 8444, while the legacy PHP version runs -on 8443. You can query one like so: +The Fast (Python) API runs on Port 8444. You can query one like so: ```sh curl -k "https://localhost:8444/rpc/?v=5&type=search&arg=python" diff --git a/docker/ca-entrypoint.sh b/docker/ca-entrypoint.sh index d03efbbc..55c7cd75 100755 --- a/docker/ca-entrypoint.sh +++ b/docker/ca-entrypoint.sh @@ -89,34 +89,26 @@ step_cert_request() { chmod 666 /data/${1}.*.pem } -if [ ! -f $DATA_ROOT_CA ]; then +if [ ! -d /root/.step/config ]; then + # Remove existing certs. + rm -vf /data/localhost.{cert,key}.pem /data/root_ca.crt + setup_step_ca install_step_ca + + start_step_ca + for host in $DATA_CERT_HOSTS; do + step_cert_request $host /data/${host}.cert.pem /data/${host}.key.pem + done + kill_step_ca + + echo -n "WARN: Your certificates are being regenerated to resolve " + echo -n "an inconsistent step-ca state. You will need to re-import " + echo "the root CA certificate into your browser." +else + exec "$@" fi -# For all hosts separated by spaces in $DATA_CERT_HOSTS, perform a check -# for their existence in /data and react accordingly. -for host in $DATA_CERT_HOSTS; do - if [ -f /data/${host}.cert.pem ] && [ -f /data/${host}.key.pem ]; then - # Found an override. Move on to running the service after - # printing a notification to the user. - echo "Found '${host}.{cert,key}.pem' override, skipping..." - echo -n "Note: If you need to regenerate certificates, run " - echo '`rm -f data/*.{cert,key}.pem` before starting this service.' - exec "$@" - else - # Otherwise, we had a missing cert or key, so remove both. - rm -f /data/${host}.cert.pem - rm -f /data/${host}.key.pem - fi -done - -start_step_ca -for host in $DATA_CERT_HOSTS; do - step_cert_request $host /data/${host}.cert.pem /data/${host}.key.pem -done -kill_step_ca - # Set permissions to /data to rwx for everybody. chmod 777 /data diff --git a/docker/cgit-entrypoint.sh b/docker/cgit-entrypoint.sh index a44675e2..282430e2 100755 --- a/docker/cgit-entrypoint.sh +++ b/docker/cgit-entrypoint.sh @@ -5,8 +5,8 @@ mkdir -p /var/data/cgit cp -vf conf/cgitrc.proto /etc/cgitrc sed -ri "s|clone-prefix=.*|clone-prefix=${CGIT_CLONE_PREFIX}|" /etc/cgitrc -sed -ri 's|header=.*|header=/aurweb/web/template/cgit/header.html|' /etc/cgitrc -sed -ri 's|footer=.*|footer=/aurweb/web/template/cgit/footer.html|' /etc/cgitrc +sed -ri 's|header=.*|header=/aurweb/static/html/cgit/header.html|' /etc/cgitrc +sed -ri 's|footer=.*|footer=/aurweb/static/html/cgit/footer.html|' /etc/cgitrc sed -ri 's|repo\.path=.*|repo.path=/aurweb/aur.git|' /etc/cgitrc sed -ri "s|^(css)=.*$|\1=${CGIT_CSS}|" /etc/cgitrc diff --git a/docker/config/aurweb-cron b/docker/config/aurweb-cron index 21fd35dc..a8e80ad6 100644 --- a/docker/config/aurweb-cron +++ b/docker/config/aurweb-cron @@ -4,4 +4,4 @@ AUR_CONFIG='/aurweb/conf/config' */2 * * * * bash -c 'aurweb-pkgmaint' */2 * * * * bash -c 'aurweb-usermaint' */2 * * * * bash -c 'aurweb-popupdate' -*/12 * * * * bash -c 'aurweb-tuvotereminder' +*/12 * * * * bash -c 'aurweb-votereminder' diff --git a/docker/config/grafana/datasources/datasource.yml b/docker/config/grafana/datasources/datasource.yml new file mode 100644 index 00000000..60a56561 --- /dev/null +++ b/docker/config/grafana/datasources/datasource.yml @@ -0,0 +1,42 @@ +--- +apiVersion: 1 + +deleteDatasources: + - name: Prometheus + - name: Tempo + +datasources: + - name: Prometheus + type: prometheus + uid: prometheus + access: proxy + url: http://prometheus:9090 + orgId: 1 + editable: false + jsonData: + timeInterval: 1m + - name: Tempo + type: tempo + uid: tempo + access: proxy + url: http://tempo:3200 + orgId: 1 + editable: false + jsonData: + tracesToMetrics: + datasourceUid: 'prometheus' + spanStartTimeShift: '1h' + spanEndTimeShift: '-1h' + serviceMap: + datasourceUid: 'prometheus' + nodeGraph: + enabled: true + search: + hide: false + traceQuery: + timeShiftEnabled: true + spanStartTimeShift: '1h' + spanEndTimeShift: '-1h' + spanBar: + type: 'Tag' + tag: 'http.path' diff --git a/docker/config/nginx.conf b/docker/config/nginx.conf index 9fdf6015..9b167553 100644 --- a/docker/config/nginx.conf +++ b/docker/config/nginx.conf @@ -27,10 +27,6 @@ http { server fastapi:8000; } - upstream cgit-php { - server cgit-php:3000; - } - upstream cgit-fastapi { server cgit-fastapi:3000; } @@ -39,54 +35,6 @@ http { server unix:/var/run/smartgit/smartgit.sock; } - server { - listen 8443 ssl http2; - server_name localhost default_server; - - ssl_certificate /etc/ssl/certs/web.cert.pem; - ssl_certificate_key /etc/ssl/private/web.key.pem; - - root /aurweb/web/html; - index index.php; - - location ~ "^/([a-z0-9][a-z0-9.+_-]*?)(\.git)?/(git-(receive|upload)-pack|HEAD|info/refs|objects/(info/(http-)?alternates|packs)|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\.(pack|idx))$" { - include uwsgi_params; - uwsgi_pass smartgit; - uwsgi_modifier1 9; - uwsgi_param SCRIPT_FILENAME /usr/lib/git-core/git-http-backend; - uwsgi_param PATH_INFO /aur.git/$3; - uwsgi_param GIT_HTTP_EXPORT_ALL ""; - uwsgi_param GIT_NAMESPACE $1; - uwsgi_param GIT_PROJECT_ROOT /aurweb; - } - - location ~ ^/cgit { - include uwsgi_params; - rewrite ^/cgit/([^?/]+/[^?]*)?(?:\?(.*))?$ /cgit.cgi?url=$1&$2 last; - uwsgi_modifier1 9; - uwsgi_param CGIT_CONFIG /etc/cgitrc; - uwsgi_pass uwsgi://cgit-php; - } - - location ~ ^/[^/]+\.php($|/) { - fastcgi_pass php-fpm:9000; - fastcgi_index index.php; - fastcgi_split_path_info ^(/[^/]+\.php)(/.*)$; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param PATH_INFO $fastcgi_path_info; - include fastcgi_params; - } - - location ~ .+\.(css|js?|jpe?g|png|svg|ico)/?$ { - try_files $uri =404; - } - - location ~ .* { - rewrite ^/(.*)$ /index.php/$1 last; - } - - } - server { listen 8444 ssl http2; server_name localhost default_server; @@ -147,4 +95,3 @@ http { '' close; } } - diff --git a/docker/config/prometheus.yml b/docker/config/prometheus.yml new file mode 100644 index 00000000..6f286dd8 --- /dev/null +++ b/docker/config/prometheus.yml @@ -0,0 +1,15 @@ +--- +global: + scrape_interval: 60s + +scrape_configs: + - job_name: tempo + static_configs: + - targets: ['tempo:3200'] + labels: + instance: tempo + - job_name: aurweb + static_configs: + - targets: ['fastapi:8000'] + labels: + instance: aurweb diff --git a/docker/config/tempo.yml b/docker/config/tempo.yml new file mode 100644 index 00000000..a94ae817 --- /dev/null +++ b/docker/config/tempo.yml @@ -0,0 +1,54 @@ +--- +stream_over_http_enabled: true +server: + http_listen_address: tempo + http_listen_port: 3200 + log_level: info + +query_frontend: + search: + duration_slo: 5s + throughput_bytes_slo: 1.073741824e+09 + trace_by_id: + duration_slo: 5s + +distributor: + receivers: + otlp: + protocols: + http: + endpoint: tempo:4318 + log_received_spans: + enabled: false + metric_received_spans: + enabled: false + +ingester: + max_block_duration: 5m + +compactor: + compaction: + block_retention: 1h + +metrics_generator: + registry: + external_labels: + source: tempo + storage: + path: /tmp/tempo/generator/wal + remote_write: + - url: http://prometheus:9090/api/v1/write + send_exemplars: true + traces_storage: + path: /tmp/tempo/generator/traces + +storage: + trace: + backend: local + wal: + path: /tmp/tempo/wal + local: + path: /tmp/tempo/blocks + +overrides: + metrics_generator_processors: [service-graphs, span-metrics, local-blocks] diff --git a/docker/health/ca.sh b/docker/health/ca.sh index 3e4bbe8e..6bf8360e 100755 --- a/docker/health/ca.sh +++ b/docker/health/ca.sh @@ -1,2 +1,2 @@ - -exec printf "" 2>>/dev/null >>/dev/tcp/127.0.0.1/8443 +#!/bin/bash +exec curl -qkiI 'https://localhost:8443/' diff --git a/docker/health/mariadb.sh b/docker/health/mariadb.sh index cbae37bd..a75089ad 100755 --- a/docker/health/mariadb.sh +++ b/docker/health/mariadb.sh @@ -1,2 +1,2 @@ #!/bin/bash -exec mysqladmin ping --silent +exec mariadb-admin ping --silent diff --git a/docker/health/memcached.sh b/docker/health/memcached.sh deleted file mode 100755 index 00f8cd98..00000000 --- a/docker/health/memcached.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -exec pgrep memcached diff --git a/docker/health/nginx.sh b/docker/health/nginx.sh index c530103d..df76bc2b 100755 --- a/docker/health/nginx.sh +++ b/docker/health/nginx.sh @@ -1,2 +1,2 @@ #!/bin/bash -exec curl --no-verify -q https://localhost:8444 +exec curl -k -q https://localhost:8444 diff --git a/docker/health/php.sh b/docker/health/php.sh deleted file mode 100755 index 7325946b..00000000 --- a/docker/health/php.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -exec printf "" >>/dev/tcp/127.0.0.1/9000 diff --git a/docker/health/prometheus.sh b/docker/health/prometheus.sh new file mode 100755 index 00000000..4917655f --- /dev/null +++ b/docker/health/prometheus.sh @@ -0,0 +1,2 @@ +#!/bin/sh +exec wget -q http://prometheus:9090/status -O /dev/null diff --git a/docker/health/tempo.sh b/docker/health/tempo.sh new file mode 100755 index 00000000..cd316662 --- /dev/null +++ b/docker/health/tempo.sh @@ -0,0 +1,2 @@ +#!/bin/sh +exec wget -q http://tempo:3200/status -O /dev/null diff --git a/docker/mariadb-entrypoint.sh b/docker/mariadb-entrypoint.sh index a00f6106..432b591b 100755 --- a/docker/mariadb-entrypoint.sh +++ b/docker/mariadb-entrypoint.sh @@ -6,26 +6,26 @@ MYSQL_DATA=/var/lib/mysql mariadb-install-db --user=mysql --basedir=/usr --datadir=$MYSQL_DATA # Start it up. -mysqld_safe --datadir=$MYSQL_DATA --skip-networking & -while ! mysqladmin ping 2>/dev/null; do +mariadbd-safe --datadir=$MYSQL_DATA --skip-networking & +while ! mariadb-admin ping 2>/dev/null; do sleep 1s done # Configure databases. -DATABASE="aurweb" # Persistent database for fastapi/php-fpm. +DATABASE="aurweb" # Persistent database for fastapi. echo "Taking care of primary database '${DATABASE}'..." -mysql -u root -e "CREATE USER IF NOT EXISTS 'aur'@'localhost' IDENTIFIED BY 'aur';" -mysql -u root -e "CREATE USER IF NOT EXISTS 'aur'@'%' IDENTIFIED BY 'aur';" -mysql -u root -e "CREATE DATABASE IF NOT EXISTS $DATABASE;" +mariadb -u root -e "CREATE USER IF NOT EXISTS 'aur'@'localhost' IDENTIFIED BY 'aur';" +mariadb -u root -e "CREATE USER IF NOT EXISTS 'aur'@'%' IDENTIFIED BY 'aur';" +mariadb -u root -e "CREATE DATABASE IF NOT EXISTS $DATABASE;" -mysql -u root -e "CREATE USER IF NOT EXISTS 'aur'@'%' IDENTIFIED BY 'aur';" -mysql -u root -e "GRANT ALL ON aurweb.* TO 'aur'@'localhost';" -mysql -u root -e "GRANT ALL ON aurweb.* TO 'aur'@'%';" +mariadb -u root -e "CREATE USER IF NOT EXISTS 'aur'@'%' IDENTIFIED BY 'aur';" +mariadb -u root -e "GRANT ALL ON aurweb.* TO 'aur'@'localhost';" +mariadb -u root -e "GRANT ALL ON aurweb.* TO 'aur'@'%';" -mysql -u root -e "CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY 'aur';" -mysql -u root -e "GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION;" +mariadb -u root -e "CREATE USER IF NOT EXISTS 'root'@'%' IDENTIFIED BY 'aur';" +mariadb -u root -e "GRANT ALL ON *.* TO 'root'@'%' WITH GRANT OPTION;" -mysqladmin -uroot shutdown +mariadb-admin -uroot shutdown exec "$@" diff --git a/docker/php-entrypoint.sh b/docker/php-entrypoint.sh deleted file mode 100755 index dc1a91de..00000000 --- a/docker/php-entrypoint.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -set -eou pipefail - -for archive in packages pkgbase users packages-meta-v1.json packages-meta-ext-v1.json; do - ln -vsf /var/lib/aurweb/archives/${archive}.gz /aurweb/web/html/${archive}.gz -done - -# Setup database. -NO_INITDB=1 /docker/mariadb-init-entrypoint.sh - -# Setup some other options. -aurweb-config set options cache 'memcache' -aurweb-config set options aur_location "$AURWEB_PHP_PREFIX" -aurweb-config set options git_clone_uri_anon "${AURWEB_PHP_PREFIX}/%s.git" -aurweb-config set options git_clone_uri_priv "${AURWEB_SSHD_PREFIX}/%s.git" - -# Listen on :9000. -sed -ri 's/^(listen).*/\1 = 0.0.0.0:9000/' /etc/php/php-fpm.d/www.conf -sed -ri 's/^;?(clear_env).*/\1 = no/' /etc/php/php-fpm.d/www.conf - -# Log to stderr. View logs via `docker-compose logs php-fpm`. -sed -ri 's|^(error_log) = .*$|\1 = /proc/self/fd/2|g' /etc/php/php-fpm.conf -sed -ri 's|^;?(access\.log) = .*$|\1 = /proc/self/fd/2|g' \ - /etc/php/php-fpm.d/www.conf - -sed -ri 's/^;?(extension=pdo_mysql)/\1/' /etc/php/php.ini -sed -ri 's/^;?(open_basedir).*$/\1 = \//' /etc/php/php.ini - -# Use the sqlite3 extension line for memcached. -sed -ri 's/^;(extension)=sqlite3$/\1=memcached/' /etc/php/php.ini - -exec "$@" diff --git a/docker/redis-entrypoint.sh b/docker/redis-entrypoint.sh index e92be6c5..669716d7 100755 --- a/docker/redis-entrypoint.sh +++ b/docker/redis-entrypoint.sh @@ -2,5 +2,6 @@ set -eou pipefail sed -ri 's/^bind .*$/bind 0.0.0.0 -::1/g' /etc/redis/redis.conf +sed -ri 's/protected-mode yes/protected-mode no/g' /etc/redis/redis.conf exec "$@" diff --git a/docker/scripts/install-deps.sh b/docker/scripts/install-deps.sh index ced18c81..0ad8937f 100755 --- a/docker/scripts/install-deps.sh +++ b/docker/scripts/install-deps.sh @@ -13,10 +13,10 @@ pacman -Sy --noconfirm --noprogressbar archlinux-keyring # Install other OS dependencies. pacman -Syu --noconfirm --noprogressbar \ - --cachedir .pkg-cache git gpgme nginx redis openssh \ + git gpgme nginx redis openssh \ mariadb mariadb-libs cgit-aurweb uwsgi uwsgi-plugin-cgi \ - php php-fpm memcached php-memcached python-pip pyalpm \ - python-srcinfo curl libeatmydata cronie python-poetry \ - python-poetry-core step-cli step-ca asciidoc + python-pip pyalpm python-srcinfo curl libeatmydata cronie \ + python-poetry python-poetry-core step-cli step-ca asciidoc \ + python-virtualenv python-pre-commit exec "$@" diff --git a/docker/scripts/install-python-deps.sh b/docker/scripts/install-python-deps.sh index 3d5f28f0..f1942498 100755 --- a/docker/scripts/install-python-deps.sh +++ b/docker/scripts/install-python-deps.sh @@ -1,11 +1,8 @@ #!/bin/bash set -eou pipefail -# Upgrade PIP; Arch Linux's version of pip is outdated for Poetry. -pip install --upgrade pip - -# Install the aurweb package and deps system-wide via poetry. -poetry config virtualenvs.create false +if [ ! -z "${COMPOSE+x}" ]; then + export PIP_BREAK_SYSTEM_PACKAGES=1 + poetry config virtualenvs.create false +fi poetry install --no-interaction --no-ansi - -exec "$@" diff --git a/docker/scripts/run-memcached.sh b/docker/scripts/run-memcached.sh deleted file mode 100755 index 90784b0f..00000000 --- a/docker/scripts/run-memcached.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -exec /usr/bin/memcached -u memcached -m 64 -c 1024 -l 0.0.0.0 diff --git a/docker/scripts/run-nginx.sh b/docker/scripts/run-nginx.sh index 6ece3303..e976f67d 100755 --- a/docker/scripts/run-nginx.sh +++ b/docker/scripts/run-nginx.sh @@ -5,8 +5,6 @@ echo echo " Services:" echo " - FastAPI : https://localhost:8444/" echo " (cgit) : https://localhost:8444/cgit/" -echo " - PHP : https://localhost:8443/" -echo " (cgit) : https://localhost:8443/cgit/" echo echo " Note: Copy root CA (./data/ca.root.pem) to ca-certificates or browser." echo diff --git a/docker/scripts/run-php.sh b/docker/scripts/run-php.sh deleted file mode 100755 index b86f8ce5..00000000 --- a/docker/scripts/run-php.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash -set -eou pipefail - -exec php-fpm --fpm-config /etc/php/php-fpm.conf --nodaemonize diff --git a/docker/scripts/run-pytests.sh b/docker/scripts/run-pytests.sh index d8c093d5..1df432f8 100755 --- a/docker/scripts/run-pytests.sh +++ b/docker/scripts/run-pytests.sh @@ -25,7 +25,7 @@ rm -rf $PROMETHEUS_MULTIPROC_DIR mkdir -p $PROMETHEUS_MULTIPROC_DIR # Run pytest with optional targets in front of it. -pytest +pytest --junitxml="/data/pytest-report.xml" # By default, report coverage and move it into cache. if [ $COVERAGE -eq 1 ]; then diff --git a/docker/scripts/run-tests.sh b/docker/scripts/run-tests.sh index a726c957..75e562b0 100755 --- a/docker/scripts/run-tests.sh +++ b/docker/scripts/run-tests.sh @@ -21,8 +21,5 @@ rm -f /data/.coverage cp -v .coverage /data/.coverage chmod 666 /data/.coverage -# Run flake8 and isort checks. -for dir in aurweb test migrations; do - flake8 --count $dir - isort --check-only $dir -done +# Run pre-commit checks +pre-commit run -a diff --git a/gunicorn.conf.py b/gunicorn.conf.py new file mode 100644 index 00000000..4f1c3a8c --- /dev/null +++ b/gunicorn.conf.py @@ -0,0 +1,7 @@ +from prometheus_client import multiprocess + + +def child_exit(server, worker): # pragma: no cover + """This function is required for gunicorn customization + of prometheus multiprocessing.""" + multiprocess.mark_process_dead(worker.pid) diff --git a/logging.conf b/logging.conf index 7dfd30f0..d27b0153 100644 --- a/logging.conf +++ b/logging.conf @@ -50,9 +50,9 @@ formatter=detailedFormatter args=(sys.stdout,) [formatter_simpleFormatter] -format=%(asctime)s %(levelname)-5s | %(name)s: %(message)s +format=%(asctime)s %(levelname)-8s | %(name)s @ (%(filename)s:%(lineno)d): %(message)s datefmt=%H:%M:%S [formatter_detailedFormatter] -format=%(asctime)s %(levelname)-5s | %(name)s.%(funcName)s() @ L%(lineno)d: %(message)s +format=%(asctime)s %(levelname)-8s | [%(name)s.%(funcName)s() @ %(filename)s:%(lineno)d]: %(message)s datefmt=%H:%M:%S diff --git a/logging.prod.conf b/logging.prod.conf new file mode 100644 index 00000000..63692a28 --- /dev/null +++ b/logging.prod.conf @@ -0,0 +1,58 @@ +[loggers] +keys=root,aurweb,uvicorn,hypercorn,alembic + +[handlers] +keys=simpleHandler,detailedHandler + +[formatters] +keys=simpleFormatter,detailedFormatter + +[logger_root] +level=INFO +; We add NullHandler programmatically. +handlers= +propogate=0 + +[logger_aurweb] +level=INFO +handlers=simpleHandler +qualname=aurweb +propagate=1 + +[logger_uvicorn] +level=WARN +handlers=simpleHandler +qualname=uvicorn +propagate=0 + +[logger_hypercorn] +level=WARN +handlers=simpleHandler +qualname=hypercorn +propagate=0 + +[logger_alembic] +level=WARN +handlers=simpleHandler +qualname=alembic +propagate=0 + +[handler_simpleHandler] +class=StreamHandler +level=INFO +formatter=simpleFormatter +args=(sys.stdout,) + +[handler_detailedHandler] +class=StreamHandler +level=DEBUG +formatter=detailedFormatter +args=(sys.stdout,) + +[formatter_simpleFormatter] +format=%(asctime)s %(levelname)-8s | %(name)s @ (%(filename)s:%(lineno)d): %(message)s +datefmt=%H:%M:%S + +[formatter_detailedFormatter] +format=%(asctime)s %(levelname)-8s | [%(name)s.%(funcName)s() @ %(filename)s:%(lineno)d]: %(message)s +datefmt=%H:%M:%S diff --git a/migrations/env.py b/migrations/env.py index 774ecdeb..dcc0329d 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -2,7 +2,6 @@ import logging import logging.config import sqlalchemy - from alembic import context import aurweb.db @@ -69,9 +68,7 @@ def run_migrations_online(): ) with connectable.connect() as connection: - context.configure( - connection=connection, target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() diff --git a/migrations/versions/38e5b9982eea_add_indicies_on_packagebases_for_rss_.py b/migrations/versions/38e5b9982eea_add_indicies_on_packagebases_for_rss_.py new file mode 100644 index 00000000..e6d5f275 --- /dev/null +++ b/migrations/versions/38e5b9982eea_add_indicies_on_packagebases_for_rss_.py @@ -0,0 +1,29 @@ +"""add indices on PackageBases for RSS order by + +Revision ID: 38e5b9982eea +Revises: 7d65d35fae45 +Create Date: 2024-08-03 01:35:39.104283 + +""" + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "38e5b9982eea" +down_revision = "7d65d35fae45" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_index("BasesModifiedTS", "PackageBases", ["ModifiedTS"], unique=False) + op.create_index("BasesSubmittedTS", "PackageBases", ["SubmittedTS"], unique=False) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index("BasesSubmittedTS", table_name="PackageBases") + op.drop_index("BasesModifiedTS", table_name="PackageBases") + # ### end Alembic commands ### diff --git a/migrations/versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py b/migrations/versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py index c3b79dab..5cbf6de8 100644 --- a/migrations/versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py +++ b/migrations/versions/56e2ce8e2ffa_utf8mb4_charset_and_collation.py @@ -5,46 +5,47 @@ Revises: ef39fcd6e1cd Create Date: 2021-05-17 14:23:00.008479 """ + from alembic import op import aurweb.config # revision identifiers, used by Alembic. -revision = '56e2ce8e2ffa' -down_revision = 'ef39fcd6e1cd' +revision = "56e2ce8e2ffa" +down_revision = "ef39fcd6e1cd" branch_labels = None depends_on = None # Tables affected by charset/collate change tables = [ - ('AccountTypes', 'utf8mb4', 'utf8mb4_general_ci'), - ('ApiRateLimit', 'utf8mb4', 'utf8mb4_general_ci'), - ('Bans', 'utf8mb4', 'utf8mb4_general_ci'), - ('DependencyTypes', 'utf8mb4', 'utf8mb4_general_ci'), - ('Groups', 'utf8mb4', 'utf8mb4_general_ci'), - ('Licenses', 'utf8mb4', 'utf8mb4_general_ci'), - ('OfficialProviders', 'utf8mb4', 'utf8mb4_bin'), - ('PackageBases', 'utf8mb4', 'utf8mb4_general_ci'), - ('PackageBlacklist', 'utf8mb4', 'utf8mb4_general_ci'), - ('PackageComments', 'utf8mb4', 'utf8mb4_general_ci'), - ('PackageDepends', 'utf8mb4', 'utf8mb4_general_ci'), - ('PackageKeywords', 'utf8mb4', 'utf8mb4_general_ci'), - ('PackageRelations', 'utf8mb4', 'utf8mb4_general_ci'), - ('PackageRequests', 'utf8mb4', 'utf8mb4_general_ci'), - ('PackageSources', 'utf8mb4', 'utf8mb4_general_ci'), - ('Packages', 'utf8mb4', 'utf8mb4_general_ci'), - ('RelationTypes', 'utf8mb4', 'utf8mb4_general_ci'), - ('RequestTypes', 'utf8mb4', 'utf8mb4_general_ci'), - ('SSHPubKeys', 'utf8mb4', 'utf8mb4_bin'), - ('Sessions', 'utf8mb4', 'utf8mb4_bin'), - ('TU_VoteInfo', 'utf8mb4', 'utf8mb4_general_ci'), - ('Terms', 'utf8mb4', 'utf8mb4_general_ci'), - ('Users', 'utf8mb4', 'utf8mb4_general_ci') + ("AccountTypes", "utf8mb4", "utf8mb4_general_ci"), + ("ApiRateLimit", "utf8mb4", "utf8mb4_general_ci"), + ("Bans", "utf8mb4", "utf8mb4_general_ci"), + ("DependencyTypes", "utf8mb4", "utf8mb4_general_ci"), + ("Groups", "utf8mb4", "utf8mb4_general_ci"), + ("Licenses", "utf8mb4", "utf8mb4_general_ci"), + ("OfficialProviders", "utf8mb4", "utf8mb4_bin"), + ("PackageBases", "utf8mb4", "utf8mb4_general_ci"), + ("PackageBlacklist", "utf8mb4", "utf8mb4_general_ci"), + ("PackageComments", "utf8mb4", "utf8mb4_general_ci"), + ("PackageDepends", "utf8mb4", "utf8mb4_general_ci"), + ("PackageKeywords", "utf8mb4", "utf8mb4_general_ci"), + ("PackageRelations", "utf8mb4", "utf8mb4_general_ci"), + ("PackageRequests", "utf8mb4", "utf8mb4_general_ci"), + ("PackageSources", "utf8mb4", "utf8mb4_general_ci"), + ("Packages", "utf8mb4", "utf8mb4_general_ci"), + ("RelationTypes", "utf8mb4", "utf8mb4_general_ci"), + ("RequestTypes", "utf8mb4", "utf8mb4_general_ci"), + ("SSHPubKeys", "utf8mb4", "utf8mb4_bin"), + ("Sessions", "utf8mb4", "utf8mb4_bin"), + ("TU_VoteInfo", "utf8mb4", "utf8mb4_general_ci"), + ("Terms", "utf8mb4", "utf8mb4_general_ci"), + ("Users", "utf8mb4", "utf8mb4_general_ci"), ] # Indexes affected by charset/collate change # Map of Unique Indexes key = index_name, value = [table_name, column1, column2] -indexes = {'ProviderNameProvides': ['OfficialProviders', 'Name', 'Provides']} +indexes = {"ProviderNameProvides": ["OfficialProviders", "Name", "Provides"]} # Source charset/collation, before this migration is run. src_charset = "utf8" diff --git a/migrations/versions/6441d3b65270_add_popularityupdated_to_packagebase.py b/migrations/versions/6441d3b65270_add_popularityupdated_to_packagebase.py new file mode 100644 index 00000000..0c817bad --- /dev/null +++ b/migrations/versions/6441d3b65270_add_popularityupdated_to_packagebase.py @@ -0,0 +1,34 @@ +"""add PopularityUpdated to PackageBase + +Revision ID: 6441d3b65270 +Revises: d64e5571bc8d +Create Date: 2022-09-22 18:08:03.280664 + +""" + +from alembic import op +from sqlalchemy.exc import OperationalError + +from aurweb.models.package_base import PackageBase +from aurweb.scripts import popupdate + +# revision identifiers, used by Alembic. +revision = "6441d3b65270" +down_revision = "d64e5571bc8d" +branch_labels = None +depends_on = None + +table = PackageBase.__table__ + + +def upgrade(): + try: + op.add_column(table.name, table.c.PopularityUpdated) + except OperationalError: + print(f"table '{table.name}' already exists, skipping migration") + + popupdate.run_variable() + + +def downgrade(): + op.drop_column(table.name, "PopularityUpdated") diff --git a/migrations/versions/6a64dd126029_rename_tu_to_package_maintainer.py b/migrations/versions/6a64dd126029_rename_tu_to_package_maintainer.py new file mode 100644 index 00000000..489d6e6c --- /dev/null +++ b/migrations/versions/6a64dd126029_rename_tu_to_package_maintainer.py @@ -0,0 +1,38 @@ +"""Rename TU to Package Maintainer + +Revision ID: 6a64dd126029 +Revises: c5a6a9b661a0 +Create Date: 2023-09-01 13:48:15.315244 + +""" + +from aurweb import db +from aurweb.models import AccountType + +# revision identifiers, used by Alembic. +revision = "6a64dd126029" +down_revision = "c5a6a9b661a0" +branch_labels = None +depends_on = None + +# AccountTypes +# ID 2 -> Trusted User / Package Maintainer +# ID 4 -> Trusted User & Developer / Package Maintainer & Developer + + +def upgrade(): + with db.begin(): + tu = db.query(AccountType).filter(AccountType.ID == 2).first() + tudev = db.query(AccountType).filter(AccountType.ID == 4).first() + + tu.AccountType = "Package Maintainer" + tudev.AccountType = "Package Maintainer & Developer" + + +def downgrade(): + with db.begin(): + pm = db.query(AccountType).filter(AccountType.ID == 2).first() + pmdev = db.query(AccountType).filter(AccountType.ID == 4).first() + + pm.AccountType = "Trusted User" + pmdev.AccountType = "Trusted User & Developer" diff --git a/migrations/versions/7d65d35fae45_rename_tu_tables_columns.py b/migrations/versions/7d65d35fae45_rename_tu_tables_columns.py new file mode 100644 index 00000000..9d768f50 --- /dev/null +++ b/migrations/versions/7d65d35fae45_rename_tu_tables_columns.py @@ -0,0 +1,48 @@ +"""Rename TU tables/columns + +Revision ID: 7d65d35fae45 +Revises: 6a64dd126029 +Create Date: 2023-09-10 10:21:33.092342 + +""" + +from alembic import op +from sqlalchemy.dialects.mysql import INTEGER + +# revision identifiers, used by Alembic. +revision = "7d65d35fae45" +down_revision = "6a64dd126029" +branch_labels = None +depends_on = None + +# TU_VoteInfo -> VoteInfo +# TU_VoteInfo.ActiveTUs -> VoteInfo.ActiveUsers +# TU_Votes -> Votes + + +def upgrade(): + # Tables + op.rename_table("TU_VoteInfo", "VoteInfo") + op.rename_table("TU_Votes", "Votes") + + # Columns + op.alter_column( + "VoteInfo", + "ActiveTUs", + existing_type=INTEGER(unsigned=True), + new_column_name="ActiveUsers", + ) + + +def downgrade(): + # Tables + op.rename_table("VoteInfo", "TU_VoteInfo") + op.rename_table("Votes", "TU_Votes") + + # Columns + op.alter_column( + "TU_VoteInfo", + "ActiveUsers", + existing_type=INTEGER(unsigned=True), + new_column_name="ActiveTUs", + ) diff --git a/migrations/versions/9e3158957fd7_add_packagekeyword_packagebaseuid.py b/migrations/versions/9e3158957fd7_add_packagekeyword_packagebaseuid.py new file mode 100644 index 00000000..86ee0067 --- /dev/null +++ b/migrations/versions/9e3158957fd7_add_packagekeyword_packagebaseuid.py @@ -0,0 +1,25 @@ +"""add PackageKeyword.PackageBaseUID index + +Revision ID: 9e3158957fd7 +Revises: 6441d3b65270 +Create Date: 2022-10-17 11:11:46.203322 + +""" + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "9e3158957fd7" +down_revision = "6441d3b65270" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_index( + "KeywordsPackageBaseID", "PackageKeywords", ["PackageBaseID"], unique=False + ) + + +def downgrade(): + op.drop_index("KeywordsPackageBaseID", table_name="PackageKeywords") diff --git a/migrations/versions/be7adae47ac3_upgrade_voteinfo_integers.py b/migrations/versions/be7adae47ac3_upgrade_voteinfo_integers.py index d910a14b..02c443a1 100644 --- a/migrations/versions/be7adae47ac3_upgrade_voteinfo_integers.py +++ b/migrations/versions/be7adae47ac3_upgrade_voteinfo_integers.py @@ -15,12 +15,13 @@ Revision ID: be7adae47ac3 Revises: 56e2ce8e2ffa Create Date: 2022-01-06 14:37:07.899778 """ + from alembic import op from sqlalchemy.dialects.mysql import INTEGER, TINYINT # revision identifiers, used by Alembic. -revision = 'be7adae47ac3' -down_revision = '56e2ce8e2ffa' +revision = "be7adae47ac3" +down_revision = "56e2ce8e2ffa" branch_labels = None depends_on = None @@ -32,7 +33,7 @@ DOWNGRADE_T = TINYINT(3, unsigned=True) def upgrade(): - """ Upgrade 'Yes', 'No', 'Abstain' and 'ActiveTUs' to unsigned INTEGER. """ + """Upgrade 'Yes', 'No', 'Abstain' and 'ActiveTUs' to unsigned INTEGER.""" op.alter_column("TU_VoteInfo", "Yes", type_=UPGRADE_T) op.alter_column("TU_VoteInfo", "No", type_=UPGRADE_T) op.alter_column("TU_VoteInfo", "Abstain", type_=UPGRADE_T) diff --git a/migrations/versions/c5a6a9b661a0_add_index_on_packagebases_popularity_.py b/migrations/versions/c5a6a9b661a0_add_index_on_packagebases_popularity_.py new file mode 100644 index 00000000..3cc146ee --- /dev/null +++ b/migrations/versions/c5a6a9b661a0_add_index_on_packagebases_popularity_.py @@ -0,0 +1,25 @@ +"""Add index on PackageBases.Popularity and .Name + +Revision ID: c5a6a9b661a0 +Revises: e4e49ffce091 +Create Date: 2023-07-02 13:46:52.522146 + +""" + +from alembic import op + +# revision identifiers, used by Alembic. +revision = "c5a6a9b661a0" +down_revision = "e4e49ffce091" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_index( + "BasesPopularityName", "PackageBases", ["Popularity", "Name"], unique=False + ) + + +def downgrade(): + op.drop_index("BasesPopularityName", table_name="PackageBases") diff --git a/migrations/versions/d64e5571bc8d_fix_pkgvote_votets.py b/migrations/versions/d64e5571bc8d_fix_pkgvote_votets.py index a89d97ef..0fda746b 100644 --- a/migrations/versions/d64e5571bc8d_fix_pkgvote_votets.py +++ b/migrations/versions/d64e5571bc8d_fix_pkgvote_votets.py @@ -5,23 +5,23 @@ Revises: be7adae47ac3 Create Date: 2022-02-18 12:47:05.322766 """ + from datetime import datetime import sqlalchemy as sa - from alembic import op from aurweb import db from aurweb.models import PackageVote # revision identifiers, used by Alembic. -revision = 'd64e5571bc8d' -down_revision = 'be7adae47ac3' +revision = "d64e5571bc8d" +down_revision = "be7adae47ac3" branch_labels = None depends_on = None table = PackageVote.__tablename__ -column = 'VoteTS' +column = "VoteTS" epoch = datetime(1970, 1, 1) diff --git a/migrations/versions/e4e49ffce091_add_hidedeletedcomments_to_user.py b/migrations/versions/e4e49ffce091_add_hidedeletedcomments_to_user.py new file mode 100644 index 00000000..6637cc0d --- /dev/null +++ b/migrations/versions/e4e49ffce091_add_hidedeletedcomments_to_user.py @@ -0,0 +1,34 @@ +"""Add HideDeletedComments to User + +Revision ID: e4e49ffce091 +Revises: 9e3158957fd7 +Create Date: 2023-04-19 23:24:25.854874 + +""" + +from alembic import op +from sqlalchemy.exc import OperationalError + +from aurweb.models.user import User + +# revision identifiers, used by Alembic. +revision = "e4e49ffce091" +down_revision = "9e3158957fd7" +branch_labels = None +depends_on = None + +table = User.__table__ + + +def upgrade(): + try: + op.add_column(table.name, table.c.HideDeletedComments) + except OperationalError: + print( + f"Column HideDeletedComments already exists in '{table.name}'," + f" skipping migration." + ) + + +def downgrade(): + op.drop_column(table.name, "HideDeletedComments") diff --git a/migrations/versions/ef39fcd6e1cd_add_sso_account_id_in_table_users.py b/migrations/versions/ef39fcd6e1cd_add_sso_account_id_in_table_users.py index 49bf055a..260c903b 100644 --- a/migrations/versions/ef39fcd6e1cd_add_sso_account_id_in_table_users.py +++ b/migrations/versions/ef39fcd6e1cd_add_sso_account_id_in_table_users.py @@ -5,32 +5,34 @@ Revises: f47cad5d6d03 Create Date: 2020-06-08 10:04:13.898617 """ -import sqlalchemy as sa +import sqlalchemy as sa from alembic import op from sqlalchemy.engine.reflection import Inspector # revision identifiers, used by Alembic. -revision = 'ef39fcd6e1cd' -down_revision = 'f47cad5d6d03' +revision = "ef39fcd6e1cd" +down_revision = "f47cad5d6d03" branch_labels = None depends_on = None def table_has_column(table, column_name): for element in Inspector.from_engine(op.get_bind()).get_columns(table): - if element.get('name') == column_name: + if element.get("name") == column_name: return True return False def upgrade(): - if not table_has_column('Users', 'SSOAccountID'): - op.add_column('Users', sa.Column('SSOAccountID', sa.String(length=255), nullable=True)) - op.create_unique_constraint(None, 'Users', ['SSOAccountID']) + if not table_has_column("Users", "SSOAccountID"): + op.add_column( + "Users", sa.Column("SSOAccountID", sa.String(length=255), nullable=True) + ) + op.create_unique_constraint(None, "Users", ["SSOAccountID"]) def downgrade(): - if table_has_column('Users', 'SSOAccountID'): - op.drop_constraint('SSOAccountID', 'Users', type_='unique') - op.drop_column('Users', 'SSOAccountID') + if table_has_column("Users", "SSOAccountID"): + op.drop_constraint("SSOAccountID", "Users", type_="unique") + op.drop_column("Users", "SSOAccountID") diff --git a/migrations/versions/f47cad5d6d03_initial_revision.py b/migrations/versions/f47cad5d6d03_initial_revision.py index b214beea..a8a007f3 100644 --- a/migrations/versions/f47cad5d6d03_initial_revision.py +++ b/migrations/versions/f47cad5d6d03_initial_revision.py @@ -4,8 +4,9 @@ Revision ID: f47cad5d6d03 Create Date: 2020-02-23 13:23:32.331396 """ + # revision identifiers, used by Alembic. -revision = 'f47cad5d6d03' +revision = "f47cad5d6d03" down_revision = None branch_labels = None depends_on = None diff --git a/po/Makefile b/po/Makefile index 0b579f48..8fd17515 100644 --- a/po/Makefile +++ b/po/Makefile @@ -48,20 +48,12 @@ all: ${MOFILES} lang=`echo $@ | sed -e 's/\.po-update$$//'`; \ msgmerge -U --no-location --lang="$$lang" $< aurweb.pot -POTFILES-php: - find ../web -type f -name '*.php' -printf '%P\n' | sort >POTFILES-php - POTFILES-py: find ../aurweb -type f -name '*.py' -printf '%P\n' | sort >POTFILES-py -update-pot: POTFILES-php POTFILES-py +update-pot: POTFILES-py pkgname=AURWEB; \ - pkgver=`sed -n 's/.*"AURWEB_VERSION", "\(.*\)".*/\1/p' ../web/lib/version.inc.php`; \ - xgettext --default-domain=aurweb -L php --keyword=__ --keyword=_n:1,2 \ - --add-location=file --add-comments=TRANSLATORS: \ - --package-name="$$pkgname" --package-version="$$pkgver" \ - --msgid-bugs-address='${MSGID_BUGS_ADDRESS}' \ - --directory ../web --files-from POTFILES-php -o aurweb.pot; \ + pkgver=`sed -n 's/version\s*=\s*"\(.*\)"/\1/p' ../pyproject.toml`; \ xgettext --default-domain=aurweb -L python --join-existing \ --keyword=translate \ --add-location=file --add-comments=TRANSLATORS: \ @@ -73,7 +65,7 @@ update-po: ${MAKE} ${UPDATEPOFILES} clean: - rm -f *.mo *.po\~ POTFILES-php POTFILES-py + rm -f *.mo *.po\~ POTFILES-py install: all for l in ${LOCALES}; do mkdir -p ${DESTDIR}${PREFIX}/$$l/LC_MESSAGES/; done @@ -82,4 +74,4 @@ install: all uninstall: for l in ${LOCALES}; do rm -rf ${DESTDIR}${PREFIX}/$$l/LC_MESSAGES/; done -.PHONY: all update-pot update-po clean install uninstall POTFILES-php POTFILES-py +.PHONY: all update-pot update-po clean install uninstall POTFILES-py diff --git a/po/ar.po b/po/ar.po index 676a5025..3bd2838c 100644 --- a/po/ar.po +++ b/po/ar.po @@ -8,11 +8,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Arabic (http://www.transifex.com/lfleischer/aurweb/language/ar/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: صفا الفليج , 2015-2016\n" +"Language-Team: Arabic (http://app.transifex.com/lfleischer/aurweb/language/ar/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -133,15 +133,15 @@ msgid "Type" msgstr "النّوع" #: html/addvote.php -msgid "Addition of a TU" -msgstr "إضافة م‌م" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "إزالة م‌م" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" msgstr "" #: html/addvote.php @@ -199,9 +199,10 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "مرحبًا بك في م‌م‌آ! فضلًا اقرأ %sإرشادات مستخدمي م‌م‌آ%s و%sإرشادات مستخدمي م‌م‌آ الموثوقين (م‌م)%s لمعلومات أكثر." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -215,8 +216,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "تذكّر أن تصوّت لحزمك المفضّلة!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "قد تكون بعض الحزم متوفّرة كثنائيّات في مستودع المجتمع [community]." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "قد تكون بعض الحزم متوفّرة كثنائيّات في مستودع المجتمع [extra]." #: html/home.php msgid "DISCLAIMER" @@ -265,8 +266,8 @@ msgstr "طلب الحذف" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "اطلب أن تُزال الحزمة من مستودع مستخدمي آرتش. فضلًا لا تستخدم هذه إن كانت الحزمة معطوبة ويمكن إصلاحها بسهولة. بدل ذلك تواصل مع مصين الحزمة وأبلغ عن طلب \"يتيمة\" إن تطلّب الأمر." +"the maintainer and file orphan request if necessary." +msgstr "" #: html/home.php msgid "Merge Request" @@ -308,10 +309,11 @@ msgstr "النّقاش" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "النّقاشات العاّمة حول مستودع مستخدمي آرتش (م‌م‌آ) وبنية المستخدمين الموثوقين تكون في %saur-general%s. للنّقاشات المتعلّقة بتطوير واجهة وِبّ م‌م‌آ، استخدم قائمة %saur-dev%s البريديّة." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -322,9 +324,9 @@ msgstr "الإبلاغ عن العلل" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "إن وجدت علّة في واجهة وِبّ م‌م‌آ، فضلًا املأ تقريرًا بها في %sمتعقّب العلل%s. استخدم المتعقّب للإبلاغ عن العلل في واجهة وِبّ م‌م‌آ %sفقط%s. للإبلاغ عن علل الحزم راسل مديرها أو اترك تعليقًا في صفحة الحزمة المناسبة." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "" #: html/home.php msgid "Package Search" @@ -524,8 +526,8 @@ msgid "Delete" msgstr "احذف" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "يمكن فقط للمستخدمين الموثوقين والمطوّرين حذف الحزم." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -565,8 +567,8 @@ msgid "Disown" msgstr "تنازل" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "يمكن فقط للمستخدمين الموثوقين والمطوّرين التّنازل عن الحزم." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -655,8 +657,8 @@ msgid "Merge" msgstr "دمج" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "يمكن فقط للمستخدمين الموثوقين والمطوّرين دمج الحزم." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -713,8 +715,8 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "مستخدم موثوق" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -725,8 +727,8 @@ msgid "Voting is closed for this proposal." msgstr "أُغلق التّصويت على هذا الرّأي." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "فقط المستخدمين الموثوقين مسموح لهم بالتّصويت." +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -1221,8 +1223,8 @@ msgstr "مطوّر" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "مستخدم موثوق ومطوّر" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1324,10 +1326,6 @@ msgstr "" msgid "Normal user" msgstr "مستخدم عاديّ" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "مستخدم موثوق" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "حساب معلّق" @@ -1400,6 +1398,15 @@ msgid "" " the Arch User Repository." msgstr "المعلومات الآتية مطلوبة فقط إن أردت تقديم حزم إلى مستودع مستخدمي آرتش." +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "مفتاح SSH العموميّ" @@ -1827,22 +1834,22 @@ msgstr "ادمج مع" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2115,8 +2122,8 @@ msgid "Registered Users" msgstr "المستخدمون المسجّلون" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "المستخدمون الموثوقون" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2301,7 +2308,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2355,3 +2362,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/ast.po b/po/ast.po index 16c363a6..94e1fd91 100644 --- a/po/ast.po +++ b/po/ast.po @@ -3,17 +3,19 @@ # This file is distributed under the same license as the AURWEB package. # # Translators: -# enolp , 2014-2015,2017 -# Ḷḷumex03 , 2014 -# prflr88 , 2014-2015 +# enolp , 2014-2015,2017,2020,2022 +# enolp , 2020 +# Ḷḷumex03, 2014 +# Ḷḷumex03, 2014 +# Pablo Lezaeta Reyes , 2014-2015 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2020-03-07 17:55+0000\n" -"Last-Translator: enolp \n" -"Language-Team: Asturian (http://www.transifex.com/lfleischer/aurweb/language/ast/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: enolp , 2014-2015,2017,2020,2022\n" +"Language-Team: Asturian (http://app.transifex.com/lfleischer/aurweb/language/ast/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -22,7 +24,7 @@ msgstr "" #: html/404.php msgid "Page Not Found" -msgstr "" +msgstr "Nun s'atopó la páxina" #: html/404.php msgid "Sorry, the page you've requested does not exist." @@ -48,7 +50,7 @@ msgstr "" #: html/503.php msgid "Service Unavailable" -msgstr "" +msgstr "El serviciu nun ta disponible" #: html/503.php msgid "" @@ -69,11 +71,11 @@ msgstr "" #: html/account.php msgid "Could not retrieve information for the specified user." -msgstr "" +msgstr "Nun se pudo recuperar la información del usuariu especificáu." #: html/account.php msgid "You do not have permission to edit this account." -msgstr "" +msgstr "Nun tienes permisu pa editar esta cuenta." #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." @@ -134,15 +136,15 @@ msgid "Type" msgstr "" #: html/addvote.php -msgid "Addition of a TU" +msgid "Addition of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU" +msgid "Removal of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" msgstr "" #: html/addvote.php @@ -200,8 +202,9 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." msgstr "" #: html/home.php @@ -216,7 +219,7 @@ msgid "Remember to vote for your favourite packages!" msgstr "" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." +msgid "Some packages may be provided as binaries in [extra]." msgstr "" #: html/home.php @@ -260,18 +263,18 @@ msgstr "" #: html/home.php msgid "Deletion Request" -msgstr "" +msgstr "Solicitú de desaniciu" #: html/home.php msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php msgid "Merge Request" -msgstr "" +msgstr "Solicitú de mecíu" #: html/home.php msgid "" @@ -304,14 +307,15 @@ msgstr "" #: html/home.php msgid "Discussion" -msgstr "" +msgstr "Discutiniu" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -323,8 +327,8 @@ msgstr "" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -473,6 +477,12 @@ msgid "" "checkbox." msgstr "" +#: aurweb/routers/packages.py +msgid "" +"The selected packages have not been adopted, check the confirmation " +"checkbox." +msgstr "" + #: html/pkgbase.php lib/pkgreqfuncs.inc.php msgid "Cannot find package to merge votes and comments into." msgstr "" @@ -519,7 +529,7 @@ msgid "Delete" msgstr "" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." +msgid "Only Package Maintainers and Developers can delete packages." msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php @@ -560,7 +570,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -571,6 +581,14 @@ msgstr "" msgid "Flag Package Out-Of-Date" msgstr "" +#: templates/packages/flag.html +msgid "" +"This seems to be a VCS package. Please do %snot%s flag it out-of-date if the" +" package version in the AUR does not match the most recent commit. Flagging " +"this package should only be done if the sources moved or changes in the " +"PKGBUILD are required because of recent upstream changes." +msgstr "" + #: html/pkgflag.php #, php-format msgid "" @@ -642,7 +660,7 @@ msgid "Merge" msgstr "" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." +msgid "Only Package Maintainers and Developers can merge packages." msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php @@ -697,22 +715,22 @@ msgstr "" #: html/tos.php msgid "I accept the terms and conditions above." -msgstr "" +msgstr "Acepto los términos y les condiciones d'arriba." #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" +msgid "Package Maintainer" msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." -msgstr "" +msgstr "Nun se pudieron recuperar los detalles de la propuesta." #: html/tu.php msgid "Voting is closed for this proposal." msgstr "" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -867,6 +885,10 @@ msgstr "" msgid "Account suspended" msgstr "" +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "" + #: lib/acctfuncs.inc.php #, php-format msgid "" @@ -910,7 +932,7 @@ msgstr "" #: lib/pkgbasefuncs.inc.php msgid "Comment cannot be empty." -msgstr "" +msgstr "El comentariu nun pue tar baleru." #: lib/pkgbasefuncs.inc.php msgid "Comment has been added." @@ -918,7 +940,7 @@ msgstr "" #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can edit package information." -msgstr "" +msgstr "Tienes d'aniciar la sesión enantes d'editar la información del paquete." #: lib/pkgbasefuncs.inc.php msgid "Missing comment ID." @@ -926,15 +948,15 @@ msgstr "" #: lib/pkgbasefuncs.inc.php msgid "No more than 5 comments can be pinned." -msgstr "" +msgstr "Nun se puen fixar más de 5 comentarios." #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to pin this comment." -msgstr "" +msgstr "Nun tienes permisu pa fixar esti comentariu." #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to unpin this comment." -msgstr "" +msgstr "Nun tienes permisu pa lliberar esti comentariu." #: lib/pkgbasefuncs.inc.php msgid "Comment has been pinned." @@ -950,6 +972,30 @@ msgstr "" #: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php msgid "Package details could not be found." +msgstr "Nun se pudieron atopar los detalles del paquete." + +#: aurweb/routers/auth.py +msgid "Bad Referer header." +msgstr "" + +#: aurweb/routers/packages.py +msgid "You did not select any packages to be notified about." +msgstr "" + +#: aurweb/routers/packages.py +msgid "The selected packages' notifications have been enabled." +msgstr "" + +#: aurweb/routers/packages.py +msgid "You did not select any packages for notification removal." +msgstr "" + +#: aurweb/routers/packages.py +msgid "A package you selected does not have notifications enabled." +msgstr "" + +#: aurweb/routers/packages.py +msgid "The selected packages' notifications have been removed." msgstr "" #: lib/pkgbasefuncs.inc.php @@ -988,6 +1034,10 @@ msgstr "" msgid "You did not select any packages to delete." msgstr "" +#: aurweb/routers/packages.py +msgid "One of the packages you selected does not exist." +msgstr "" + #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been deleted." msgstr "" @@ -996,10 +1046,18 @@ msgstr "" msgid "You must be logged in before you can adopt packages." msgstr "" +#: aurweb/routers/package.py +msgid "You are not allowed to adopt one of the packages you selected." +msgstr "" + #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can disown packages." msgstr "" +#: aurweb/routers/packages.py +msgid "You are not allowed to disown one of the packages you selected." +msgstr "" + #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to adopt." msgstr "" @@ -1168,7 +1226,7 @@ msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1271,10 +1329,6 @@ msgstr "" msgid "Normal user" msgstr "" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "" @@ -1347,6 +1401,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1551,7 +1614,7 @@ msgstr "" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php msgid "Keywords" -msgstr "" +msgstr "Pallabres clave" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php @@ -1770,22 +1833,22 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -1805,7 +1868,7 @@ msgstr[1] "" #: template/pkgreq_results.php template/pkg_search_results.php #, php-format msgid "Page %d of %d." -msgstr "" +msgstr "Páxina %d de %d." #: template/pkgreq_results.php msgid "Package" @@ -2019,7 +2082,7 @@ msgstr "" #: template/stats/general_stats_table.php msgid "Orphan Packages" -msgstr "" +msgstr "Paquetes güérfanos" #: template/stats/general_stats_table.php msgid "Packages added in the past 7 days" @@ -2039,15 +2102,15 @@ msgstr "" #: template/stats/general_stats_table.php msgid "Registered Users" -msgstr "" +msgstr "Usuarios rexistraos" #: template/stats/general_stats_table.php -msgid "Trusted Users" +msgid "Package Maintainers" msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" -msgstr "" +msgstr "Anovamientos de recién" #: template/stats/updates_table.php msgid "more" @@ -2092,7 +2155,7 @@ msgstr "" #: template/tu_details.php msgid "Participation" -msgstr "" +msgstr "Participación" #: template/tu_last_votes_list.php msgid "Last Votes by TU" @@ -2228,7 +2291,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2237,3 +2300,80 @@ msgid "" "Please remember to cast your vote on proposal {id} [1]. The voting period " "ends in less than 48 hours." msgstr "" + +#: aurweb/routers/accounts.py +msgid "Invalid account type provided." +msgstr "" + +#: aurweb/routers/accounts.py +msgid "You do not have permission to change account types." +msgstr "" + +#: aurweb/routers/accounts.py +msgid "You do not have permission to change this user's account type to %s." +msgstr "" + +#: aurweb/packages/requests.py +msgid "No due existing orphan requests to accept for %s." +msgstr "" + +#: aurweb/asgi.py +msgid "Internal Server Error" +msgstr "Fallu internu del sirvidor" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +msgstr "Asocedió un fallu fatal." + +#: templates/errors/500.html +msgid "" +"Details have been logged and will be reviewed by the postmaster posthaste. " +"We apologize for any inconvenience this may have caused." +msgstr "" + +#: aurweb/scripts/notify.py +msgid "AUR Server Error" +msgstr "" + +#: templates/pkgbase/merge.html templates/packages/delete.html +#: templates/packages/disown.html +msgid "Related package request closure comments..." +msgstr "" + +#: templates/pkgbase/merge.html templates/packages/delete.html +msgid "" +"This action will close any pending package requests related to it. If " +"%sComments%s are omitted, a closure comment will be autogenerated." +msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/aurweb.pot b/po/aurweb.pot index bec1b672..b1a467e4 100644 --- a/po/aurweb.pot +++ b/po/aurweb.pot @@ -132,15 +132,15 @@ msgid "Type" msgstr "" #: html/addvote.php -msgid "Addition of a TU" +msgid "Addition of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU" +msgid "Removal of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" msgstr "" #: html/addvote.php @@ -198,8 +198,9 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." msgstr "" #: html/home.php @@ -214,7 +215,7 @@ msgid "Remember to vote for your favourite packages!" msgstr "" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." +msgid "Some packages may be provided as binaries in [extra]." msgstr "" #: html/home.php @@ -264,7 +265,7 @@ msgstr "" msgid "" "Request a package to be removed from the Arch User Repository. Please do not " "use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -307,7 +308,7 @@ msgstr "" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User " +"General discussion regarding the Arch User Repository (AUR) and Package Maintainer " "structure takes place on %saur-general%s. For discussion relating to the " "development of the AUR web interface, use the %saur-dev%s mailing list." msgstr "" @@ -321,7 +322,7 @@ msgstr "" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface " -"%sonly%s. To report packaging bugs contact the package maintainer or leave a " +"%sonly%s. To report packaging bugs contact the maintainer or leave a " "comment on the appropriate package page." msgstr "" @@ -522,7 +523,7 @@ msgid "Delete" msgstr "" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." +msgid "Only Package Maintainers and Developers can delete packages." msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php @@ -563,7 +564,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -654,7 +655,7 @@ msgid "Merge" msgstr "" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." +msgid "Only Package Maintainers and Developers can merge packages." msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php @@ -712,7 +713,7 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" +msgid "Package Maintainer" msgstr "" #: html/tu.php @@ -724,7 +725,7 @@ msgid "Voting is closed for this proposal." msgstr "" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -1220,7 +1221,7 @@ msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1322,10 +1323,6 @@ msgstr "" msgid "Normal user" msgstr "" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "" @@ -1398,6 +1395,14 @@ msgid "" "the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1821,14 +1826,14 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " +"By submitting a deletion request, you ask a Package Maintainer to delete the " "package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " +"By submitting a merge request, you ask a Package Maintainer to delete the package " "base and transfer its votes and comments to another package base. Merging a " "package does not affect the corresponding Git repositories. Make sure you " "update the Git history of the target package yourself." @@ -1836,7 +1841,7 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2092,7 +2097,7 @@ msgid "Registered Users" msgstr "" #: template/stats/general_stats_table.php -msgid "Trusted Users" +msgid "Package Maintainers" msgstr "" #: template/stats/updates_table.php @@ -2279,7 +2284,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2334,3 +2339,39 @@ msgid "This action will close any pending package requests " "related to it. If %sComments%s are omitted, a closure " "comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "Note that if you hide your email address, it'll " +"end up on the BCC list for any request notifications. " +"In case someone replies to these notifications, you won't " +"receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Maximum number of characters" +msgstr "" diff --git a/po/az.po b/po/az.po index 7e534b4c..4f8e0ba8 100644 --- a/po/az.po +++ b/po/az.po @@ -6,11 +6,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Azerbaijani (http://www.transifex.com/lfleischer/aurweb/language/az/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Azerbaijani (http://app.transifex.com/lfleischer/aurweb/language/az/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -131,15 +131,15 @@ msgid "Type" msgstr "" #: html/addvote.php -msgid "Addition of a TU" +msgid "Addition of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU" +msgid "Removal of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" msgstr "" #: html/addvote.php @@ -197,8 +197,9 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." msgstr "" #: html/home.php @@ -213,7 +214,7 @@ msgid "Remember to vote for your favourite packages!" msgstr "" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." +msgid "Some packages may be provided as binaries in [extra]." msgstr "" #: html/home.php @@ -263,7 +264,7 @@ msgstr "" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -306,9 +307,10 @@ msgstr "" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -320,8 +322,8 @@ msgstr "" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -522,7 +524,7 @@ msgid "Delete" msgstr "" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." +msgid "Only Package Maintainers and Developers can delete packages." msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php @@ -563,7 +565,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -653,7 +655,7 @@ msgid "Merge" msgstr "" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." +msgid "Only Package Maintainers and Developers can merge packages." msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php @@ -711,7 +713,7 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" +msgid "Package Maintainer" msgstr "" #: html/tu.php @@ -723,7 +725,7 @@ msgid "Voting is closed for this proposal." msgstr "" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -1219,7 +1221,7 @@ msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1322,10 +1324,6 @@ msgstr "" msgid "Normal user" msgstr "" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "" @@ -1398,6 +1396,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1821,22 +1828,22 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2093,7 +2100,7 @@ msgid "Registered Users" msgstr "" #: template/stats/general_stats_table.php -msgid "Trusted Users" +msgid "Package Maintainers" msgstr "" #: template/stats/updates_table.php @@ -2279,7 +2286,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2333,3 +2340,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/az_AZ.po b/po/az_AZ.po index e903027b..3f65d694 100644 --- a/po/az_AZ.po +++ b/po/az_AZ.po @@ -6,11 +6,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Azerbaijani (Azerbaijan) (http://www.transifex.com/lfleischer/aurweb/language/az_AZ/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Azerbaijani (Azerbaijan) (http://app.transifex.com/lfleischer/aurweb/language/az_AZ/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -131,15 +131,15 @@ msgid "Type" msgstr "" #: html/addvote.php -msgid "Addition of a TU" +msgid "Addition of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU" +msgid "Removal of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" msgstr "" #: html/addvote.php @@ -197,8 +197,9 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." msgstr "" #: html/home.php @@ -213,7 +214,7 @@ msgid "Remember to vote for your favourite packages!" msgstr "" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." +msgid "Some packages may be provided as binaries in [extra]." msgstr "" #: html/home.php @@ -263,7 +264,7 @@ msgstr "" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -306,9 +307,10 @@ msgstr "" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -320,8 +322,8 @@ msgstr "" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -522,7 +524,7 @@ msgid "Delete" msgstr "" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." +msgid "Only Package Maintainers and Developers can delete packages." msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php @@ -563,7 +565,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -653,7 +655,7 @@ msgid "Merge" msgstr "" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." +msgid "Only Package Maintainers and Developers can merge packages." msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php @@ -711,7 +713,7 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" +msgid "Package Maintainer" msgstr "" #: html/tu.php @@ -723,7 +725,7 @@ msgid "Voting is closed for this proposal." msgstr "" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -1219,7 +1221,7 @@ msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1322,10 +1324,6 @@ msgstr "" msgid "Normal user" msgstr "" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "" @@ -1398,6 +1396,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1821,22 +1828,22 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2093,7 +2100,7 @@ msgid "Registered Users" msgstr "" #: template/stats/general_stats_table.php -msgid "Trusted Users" +msgid "Package Maintainers" msgstr "" #: template/stats/updates_table.php @@ -2279,7 +2286,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2333,3 +2340,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/bg.po b/po/bg.po index 7864f5dc..22a68d8d 100644 --- a/po/bg.po +++ b/po/bg.po @@ -6,11 +6,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Bulgarian (http://www.transifex.com/lfleischer/aurweb/language/bg/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Bulgarian (http://app.transifex.com/lfleischer/aurweb/language/bg/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -131,15 +131,15 @@ msgid "Type" msgstr "" #: html/addvote.php -msgid "Addition of a TU" +msgid "Addition of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU" +msgid "Removal of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" msgstr "" #: html/addvote.php @@ -197,8 +197,9 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." msgstr "" #: html/home.php @@ -213,7 +214,7 @@ msgid "Remember to vote for your favourite packages!" msgstr "" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." +msgid "Some packages may be provided as binaries in [extra]." msgstr "" #: html/home.php @@ -263,7 +264,7 @@ msgstr "" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -306,9 +307,10 @@ msgstr "" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -320,8 +322,8 @@ msgstr "" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -522,7 +524,7 @@ msgid "Delete" msgstr "" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." +msgid "Only Package Maintainers and Developers can delete packages." msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php @@ -563,7 +565,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -653,7 +655,7 @@ msgid "Merge" msgstr "" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." +msgid "Only Package Maintainers and Developers can merge packages." msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php @@ -711,7 +713,7 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" +msgid "Package Maintainer" msgstr "" #: html/tu.php @@ -723,7 +725,7 @@ msgid "Voting is closed for this proposal." msgstr "" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -1219,7 +1221,7 @@ msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1322,10 +1324,6 @@ msgstr "" msgid "Normal user" msgstr "" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "" @@ -1398,6 +1396,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1821,22 +1828,22 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2093,7 +2100,7 @@ msgid "Registered Users" msgstr "" #: template/stats/general_stats_table.php -msgid "Trusted Users" +msgid "Package Maintainers" msgstr "" #: template/stats/updates_table.php @@ -2279,7 +2286,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2333,3 +2340,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/ca.po b/po/ca.po index 391dd146..10fd8412 100644 --- a/po/ca.po +++ b/po/ca.po @@ -10,11 +10,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Catalan (http://www.transifex.com/lfleischer/aurweb/language/ca/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Ícar , 2021\n" +"Language-Team: Catalan (http://app.transifex.com/lfleischer/aurweb/language/ca/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -135,16 +135,16 @@ msgid "Type" msgstr "Tipus" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Adició d'un TU" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Elimincació d'un TU" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Elimincació d'un TU (inactivitat no declarada)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -201,9 +201,10 @@ msgstr "Cerca paquets que co-mantinc" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Benvingut a l'AUR! Si us plau, llegiu les %sdirectrius d'usuari d'AUR%s i les %sdirectrius de TU (usuari de confiança) d'AUR%s per més informació." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -217,8 +218,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "Recordeu votar els vostres paquets preferits!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Alguns paquets poden ser oferts com binaris a [community]." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Alguns paquets poden ser oferts com binaris a [extra]." #: html/home.php msgid "DISCLAIMER" @@ -267,8 +268,8 @@ msgstr "Sol·licitud de supressió" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "Sol·liciteu que s'elimini un paquet de l'AUR. Si us plau, no ho utilitzeu si un paquet està trencat i es pot arreglar fàcilment. En lloc d'això, poseu-vos en contacte amb el mantenidor del paquet i sol·liciteu que es se'n renegui si cal." +"the maintainer and file orphan request if necessary." +msgstr "" #: html/home.php msgid "Merge Request" @@ -310,10 +311,11 @@ msgstr "Discussió" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "El debat general sobre el repositori per usuaris d'Arch (AUR) i de l'estructura dels Usuaris de Confiança es realitza a %saur-general%s. Per la discussió relacionada amb el desenvolupament de la interfície web de l'AUR, utilitzeu la llista de correu %saur-dev%s." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -324,9 +326,9 @@ msgstr "Comunicar errada" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "Si troba un error en la interfície web de l'AUR, si us plau, ompliu un informe d'error al nostre %srastrejador d'errades%s. Utilitzeu el rastrejador per reportar %snomés%s errors de l'interfície web. Per informar d'errades en els paquets contacteu directament amb el responsable del paquet o deixeu un comentari a la pàgina del paquet corresponent." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "" #: html/home.php msgid "Package Search" @@ -526,8 +528,8 @@ msgid "Delete" msgstr "Esborra" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Només els usuaris de confiança (TUs) i desenvolupadors poden eliminar paquets." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -567,7 +569,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -657,8 +659,8 @@ msgid "Merge" msgstr "Fusió" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Només el usuaris de confiança (TUs) i desenvolupadors poden fusionar paquets." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -715,8 +717,8 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Usuari de Confiança" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -727,7 +729,7 @@ msgid "Voting is closed for this proposal." msgstr "La votació es va tancar per a aquesta proposta." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -1223,7 +1225,7 @@ msgstr "Desenvolupador" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1326,10 +1328,6 @@ msgstr "" msgid "Normal user" msgstr "Usuari normal" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Usuari de Confiança" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "El compte s'ha suspès" @@ -1402,6 +1400,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1825,22 +1832,22 @@ msgstr "Combinar amb" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2097,8 +2104,8 @@ msgid "Registered Users" msgstr "Usuaris registrats" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Usuaris de Confiança" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2283,7 +2290,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2337,3 +2344,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/ca_ES.po b/po/ca_ES.po index bad69bd1..3927d5a2 100644 --- a/po/ca_ES.po +++ b/po/ca_ES.po @@ -6,11 +6,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Catalan (Spain) (http://www.transifex.com/lfleischer/aurweb/language/ca_ES/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Catalan (Spain) (http://app.transifex.com/lfleischer/aurweb/language/ca_ES/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -131,15 +131,15 @@ msgid "Type" msgstr "" #: html/addvote.php -msgid "Addition of a TU" +msgid "Addition of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU" +msgid "Removal of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" msgstr "" #: html/addvote.php @@ -197,8 +197,9 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." msgstr "" #: html/home.php @@ -213,7 +214,7 @@ msgid "Remember to vote for your favourite packages!" msgstr "" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." +msgid "Some packages may be provided as binaries in [extra]." msgstr "" #: html/home.php @@ -263,7 +264,7 @@ msgstr "" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -306,9 +307,10 @@ msgstr "" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -320,8 +322,8 @@ msgstr "" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -522,7 +524,7 @@ msgid "Delete" msgstr "" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." +msgid "Only Package Maintainers and Developers can delete packages." msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php @@ -563,7 +565,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -653,7 +655,7 @@ msgid "Merge" msgstr "" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." +msgid "Only Package Maintainers and Developers can merge packages." msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php @@ -711,7 +713,7 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" +msgid "Package Maintainer" msgstr "" #: html/tu.php @@ -723,7 +725,7 @@ msgid "Voting is closed for this proposal." msgstr "" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -1219,7 +1221,7 @@ msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1322,10 +1324,6 @@ msgstr "" msgid "Normal user" msgstr "" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "" @@ -1398,6 +1396,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1821,22 +1828,22 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2093,7 +2100,7 @@ msgid "Registered Users" msgstr "" #: template/stats/general_stats_table.php -msgid "Trusted Users" +msgid "Package Maintainers" msgstr "" #: template/stats/updates_table.php @@ -2279,7 +2286,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2333,3 +2340,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/cs.po b/po/cs.po index b9bd739a..f12e07ab 100644 --- a/po/cs.po +++ b/po/cs.po @@ -5,21 +5,21 @@ # Translators: # Daniel Milde , 2017 # Daniel Peukert , 2021 -# Daniel Peukert , 2021 -# Jaroslav Lichtblau , 2015-2016 -# Jaroslav Lichtblau , 2014 -# Jiří Vírava , 2017-2018 +# Daniel Peukert , 2021-2022 +# Jaroslav Lichtblau , 2015-2016 +# Jaroslav Lichtblau , 2014 +# Appukonrad , 2017-2018 # Lukas Fleischer , 2011 # Lukáš Kucharczyk , 2020 # Pavel Ševeček , 2014 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Czech (http://www.transifex.com/lfleischer/aurweb/language/cs/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Daniel Peukert , 2021-2022\n" +"Language-Team: Czech (http://app.transifex.com/lfleischer/aurweb/language/cs/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -140,16 +140,16 @@ msgid "Type" msgstr "Typ" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Přidání důvěryhodného uživatele" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Odebrání důvěryhodného uživatele" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Odebrání důvěryhodného uživatele (neohlášená neaktivita)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -206,9 +206,10 @@ msgstr "Hledat balíčky, které spoluspravuji" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Vítejte na webu repozitáře AUR! Další informace naleznete na wiki v článcích o %srepozitáři AUR%s a %sdůvěryhodných uživatelích AUR%s." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -222,8 +223,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "Nezapomeň hlasovat pro svoje oblíbené balíčky!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Některé balíčky mohou být poskytnuty v binární podobě v repozitáři [community]." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Některé balíčky mohou být poskytnuty v binární podobě v repozitáři [extra]." #: html/home.php msgid "DISCLAIMER" @@ -272,8 +273,8 @@ msgstr "Žádost o smazání" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "Požádá o smazání z repozitáře AUR. Nepoužívejte, pokud balíček nefunguje a je možné ho snadno opravit. Místo toho kontaktuje správce balíčku nebo v případě potřeby požádejte o odebrání vlastnictví." +"the maintainer and file orphan request if necessary." +msgstr "" #: html/home.php msgid "Merge Request" @@ -315,10 +316,11 @@ msgstr "Diskuze" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "Obecná diskuze o struktuře repozitáře AUR a důvěryhodných uživatelích se odehrává v poštovní konferenci %saur-general%s. Diskuzi o vývoji webového rozhraní AUR pak naleznete v poštovní konferenci %saur-dev%s." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -329,9 +331,9 @@ msgstr "Hlášení chyb" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "Pokud ve webovém rozhraní repozitáře AUR najdete chybu, nahlaste to na %swebu pro sledování chyb%s. Zmíněný web se používá %sjen%s pro chyby webového rozhraní AUR. Pokud chcete nahlásit chyby v balíčcích, kontaktujte správce daného balíčku nebo k balíčku napište komentář." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "" #: html/home.php msgid "Package Search" @@ -531,8 +533,8 @@ msgid "Delete" msgstr "Smazat" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Pouze důvěryhodní uživatelé a vývojáři mohou mazat balíčky." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -572,8 +574,8 @@ msgid "Disown" msgstr "Odebrat vlastnictví" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "Pouze důvěryhodní uživatelé a vývojáři mohou odebrat vlastnictví balíčku." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -662,8 +664,8 @@ msgid "Merge" msgstr "Spojit" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Pouze důvěryhodní uživatelé a vývojáři mohou slučovat balíčky." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -720,8 +722,8 @@ msgid "I accept the terms and conditions above." msgstr "Souhlasím s výše uvedenými podmínkami." #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Důvěryhodný uživatel" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -732,8 +734,8 @@ msgid "Voting is closed for this proposal." msgstr "Toto hlasování již skončilo." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Pouze důvěryhodní uživatelé a vývojáři mohou hlasovat." +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -763,7 +765,7 @@ msgstr "Hlasující" msgid "" "Account registration has been disabled for your IP address, probably due to " "sustained spam attacks. Sorry for the inconvenience." -msgstr "Registrace účtu byla pro vaši IP adresu zakázána, pravděpodobně kvůli trvalým spamovým útokům. Omluvám se za nepříjemnost." +msgstr "Registrace účtu byla pro vaši IP adresu zakázána, pravděpodobně kvůli trvalým spamovým útokům. Za nepříjemnosti se omlouváme." #: lib/acctfuncs.inc.php msgid "Missing User ID" @@ -978,7 +980,7 @@ msgstr "Informace o balíčku nebyly nalezeny." #: aurweb/routers/auth.py msgid "Bad Referer header." -msgstr "" +msgstr "Chybná hlavička Referer" #: aurweb/routers/packages.py msgid "You did not select any packages to be notified about." @@ -1228,8 +1230,8 @@ msgstr "Vyvojář" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "Důvěryhodný uživatel a vývojář" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1331,10 +1333,6 @@ msgstr "Vaše uživatelské jméno je vaše přihlašovací jméno. Je veřejně msgid "Normal user" msgstr "Běžný uživatel" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Důvěryhodný uživatel" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Účet pozastaven" @@ -1407,6 +1405,15 @@ msgid "" " the Arch User Repository." msgstr "Následující informace jsou požadovány pouze v případě, že máte v plánu do uživatelského repozitáře systému Arch Linux přidávat balíčky." +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "Veřejný SSH klíč" @@ -1832,26 +1839,26 @@ msgstr "Sloučení" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "Vytvořením žádosti o smazání žádáte důvěryhodného uživatele, aby smazal základní balíček. Tento typ požadavku by se měl používat pro duplicitní balíčky, software, který již v upstreamu neexistuje, nebo v případě nelegálních či nezvratně rozbitých balíčků." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "Vytvořením žádosti o sloučení žádáte důvěryhodného uživatele, aby smazal základní balíček a přesunul s ním spojené hlasy a komentáře do jiného balíčku. Sloučení balíčku nemá vliv na související repozitáře Git. Aktualizace Git historie cílového balíčku je na vás." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "Vytvořením žádosti o odebrání vlastnictví žádáte důvěryhodného uživatele, aby odebral vlastnictví aktuálnímu správci základního balíčku. Tento požadavek používejte jen v případě, že balíček potřebuje zásah správce, správce není k zastižení a již jste se správce pokusili v minulosti kontaktovat." +msgstr "" #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -2112,8 +2119,8 @@ msgid "Registered Users" msgstr "Registrovaní uživatelé" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Důvěryhodní uživatelé" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2298,8 +2305,8 @@ msgstr "Uživatel {user} [1] smazal balíček {pkgbase} [2].\n\nNadále již neb #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" -msgstr "Připomínka k hlasování důvěryhodných uživatelů: návrh {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" +msgstr "" #: scripts/notify.py #, python-brace-format @@ -2322,33 +2329,65 @@ msgstr "Pro změnu typu tohoto účtu na %s nemáte oprávnění." #: aurweb/packages/requests.py msgid "No due existing orphan requests to accept for %s." -msgstr "" +msgstr "Žádné žádosti o odebrání vlastnictví balíčku %s momentálně neexistují." #: aurweb/asgi.py msgid "Internal Server Error" -msgstr "" +msgstr "Interní chyba serveru" #: templates/errors/500.html msgid "A fatal error has occurred." -msgstr "" +msgstr "Došlo k fatální chybě." #: templates/errors/500.html msgid "" "Details have been logged and will be reviewed by the postmaster posthaste. " "We apologize for any inconvenience this may have caused." -msgstr "" +msgstr "Detaily chyby byly zalogovány a budou co nejdříve zkontrolovány administrátorem. Za jakékoli způsobené nepříjemnosti se omlouváme." #: aurweb/scripts/notify.py msgid "AUR Server Error" -msgstr "" +msgstr "Chyba serveru AUR" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "Komentáře k uzavření žádostí vztahujících se k tomuto balíčku..." #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." +msgstr "Tato akce uzavře všechny žádosti čekající na vyřízení vztahující se k tomuto balíčku. Pokud není vyplněno textové Pole %sKomentáře\"%s, komentář k uzavření žádostí bude vygenerován automaticky." + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." msgstr "" diff --git a/po/da.po b/po/da.po index a6f290ea..b8768beb 100644 --- a/po/da.po +++ b/po/da.po @@ -9,11 +9,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Danish (http://www.transifex.com/lfleischer/aurweb/language/da/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Linuxbruger , 2018\n" +"Language-Team: Danish (http://app.transifex.com/lfleischer/aurweb/language/da/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -134,16 +134,16 @@ msgid "Type" msgstr "Type" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Tilføjelse af en TU" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Bortskaffelse af en TU" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Bortskaffelse af en TU (ikke erklæret inaktivitet)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -200,9 +200,10 @@ msgstr "Søg efter pakker jeg co-vedligeholder" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Velkommen til AUR! Venligst læs %sAUR Bruger Retningslinier %s og %sAUR TU Retningslinier%s for mere information." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -216,7 +217,7 @@ msgid "Remember to vote for your favourite packages!" msgstr "Husk at stemme for dine favorit pakker!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." +msgid "Some packages may be provided as binaries in [extra]." msgstr "Nogle pakker kan være stillet til rådighed som binær i (fællesskab)." #: html/home.php @@ -266,7 +267,7 @@ msgstr "Sletning Forespørgsel" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -309,9 +310,10 @@ msgstr "Diskussion" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -323,8 +325,8 @@ msgstr "" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -525,8 +527,8 @@ msgid "Delete" msgstr "Slet" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Kun betroede brugere og udviklere kan slette pakker." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -566,7 +568,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -656,7 +658,7 @@ msgid "Merge" msgstr "" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." +msgid "Only Package Maintainers and Developers can merge packages." msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php @@ -714,8 +716,8 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Betroet bruger (TU)" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -726,7 +728,7 @@ msgid "Voting is closed for this proposal." msgstr "Afstemningen er lukket for dette forslag." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -1222,8 +1224,8 @@ msgstr "Udvikler" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "Betroet bruger og udvikler" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1325,10 +1327,6 @@ msgstr "" msgid "Normal user" msgstr "Normal bruger" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Betroet bruger (TU)" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Konto suspenderet" @@ -1401,6 +1399,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1824,22 +1831,22 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2096,8 +2103,8 @@ msgid "Registered Users" msgstr "Registerede brugere" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Betroede brugere" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2282,7 +2289,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2336,3 +2343,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/de.po b/po/de.po index ec0a0fbe..39326eac 100644 --- a/po/de.po +++ b/po/de.po @@ -27,11 +27,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: German (http://www.transifex.com/lfleischer/aurweb/language/de/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Stefan Auditor , 2021\n" +"Language-Team: German (http://app.transifex.com/lfleischer/aurweb/language/de/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -152,16 +152,16 @@ msgid "Type" msgstr "Typ" #: html/addvote.php -msgid "Addition of a TU" -msgstr "TU hinzugefügt" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "TU entfernt" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "TU entfernt (unerklärte Inaktivität)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -218,9 +218,10 @@ msgstr "Suche nach Paketen die ich mitbetreue" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Willkommen im AUR! Für weitere Informationen lies bitte die %sAUR User Guidelines%s und die %sAUR TU Guidelines%s." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -234,8 +235,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "Denk daran, für Deine bevorzugten Pakete zu stimmen!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Manche Pakete könnten als Binär-Pakete in [community] bereitgestellt sein." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Manche Pakete könnten als Binär-Pakete in [extra] bereitgestellt sein." #: html/home.php msgid "DISCLAIMER" @@ -284,8 +285,8 @@ msgstr "Löschanfrage" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "Anfrage zum Entfernen eines Pakets aus dem Arch User Repository. Bitte benutze diese nicht, wenn das Paket kaputt ist und leicht repariert werden kann. Kontaktiere stattdessen den Maintainer und reiche eine Verwaisungsanfrage ein, wenn nötig." +"the maintainer and file orphan request if necessary." +msgstr "" #: html/home.php msgid "Merge Request" @@ -327,10 +328,11 @@ msgstr "Diskussion" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "Grundsätzliche Diskussionen bezüglich des Arch User Repository (AUR) und vertrauenswürdigen Benutzern finden auf %saur-general%s statt. Für Diskussionen im Bezug auf die Entwicklung des AUR Web-Interface benutze die %saur-dev%s Mailingliste." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -341,9 +343,9 @@ msgstr "Fehler melden" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "Wenn du einen Fehler im AUR Web-Interface findest, fülle bitte eine Fehler-Meldung in unserem %sbug-tracker%s aus. Benutze den Tracker %snur%s, um Fehler im AUR zu melden. Für falsch gepackte Pakete, wende dich bitte direkt an den zuständigen Maintainer, oder hinterlasse einen Kommentar auf der entsprechenden Seite des Pakets." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "" #: html/home.php msgid "Package Search" @@ -543,8 +545,8 @@ msgid "Delete" msgstr "Löschen" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Nur vertrauenswürdige Benutzer und Entwickler können Pakete löschen." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -584,8 +586,8 @@ msgid "Disown" msgstr "Gebe Paket ab" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "Nur TUs und Developer können die Paket-Betreuung abgeben." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -674,8 +676,8 @@ msgid "Merge" msgstr "Verschmelzen" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Nur vertrauenswürdige Benutzer und Entwickler können Pakete verschmelzen." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -732,8 +734,8 @@ msgid "I accept the terms and conditions above." msgstr "Ich akzeptiere die obigen Nutzungsbedingungen." #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Vertrauenswürdiger Benutzer (TU)" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -744,8 +746,8 @@ msgid "Voting is closed for this proposal." msgstr "Die Abstimmungsphase für diesen Vorschlag ist beendet." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Nur Trusted User dürfen wählen." +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -1240,8 +1242,8 @@ msgstr "Entwickler" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "Vertrauenswürdiger Benutzer & Entwickler" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1343,10 +1345,6 @@ msgstr "Der Benutzername ist der Name, der für die Anmeldung benutzt wird. Er i msgid "Normal user" msgstr "Normaler Benutzer" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Vertrauenswürdiger Benutzer (TU)" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Konto gesperrt" @@ -1419,6 +1417,15 @@ msgid "" " the Arch User Repository." msgstr "Die folgende Information wird nur benötigt, wenn du Pakete beim Arch User Repository einreichen willst." +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "Öffentlicher SSH Schlüssel" @@ -1842,26 +1849,26 @@ msgstr "Verschmelzen mit" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "Durch das Absenden einer Löschanfrage wird ein vertrauenswürdiger Benutzer gefragt die Paketbasis zu löschen. Dieser Typ von Anfragen soll für doppelte Pakete, vom Upstream aufgegebene Software sowie illegale und unreparierbar kaputte Pakete verwendet werden." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "Durch das Absenden einer Zusammenfüranfrage wird ein vertrauenswürdiger Benutzer gefragt die Paketbasis zu löschen und die Stimmen und Kommentare zu einer anderen Paketbasis zu transferieren. Das Zusammenführen eines Pakets betrifft nicht die zugehörigen Git-Repositories. Stelle sicher, dass die Git-Historie des Zielpakets von dir aktualisiert wird." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "Durch das absenden einer Verwaisanfrage wird ein vertrauenswürdiger Benutzer gebeten die Paketbasis zu enteignen. Bitte tue das nur, wenn das Paket Aufmerksamkeit des Maintainers benötigt, der Maintainer nicht reagiert und Du vorher bereits versucht hast ihn zu kontaktieren." +msgstr "" #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -2114,8 +2121,8 @@ msgid "Registered Users" msgstr "Registrierte Benutzer" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Vertrauenswürdige Benutzer (TU)" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2300,8 +2307,8 @@ msgstr "{user} [1] hat {pkgbase} [2] gelöscht.\n\nDu wirst keine weiteren Benac #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" -msgstr "TU Abstimmungs-Erinnerung: Vorschlag {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" +msgstr "" #: scripts/notify.py #, python-brace-format @@ -2354,3 +2361,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/el.po b/po/el.po index f1fe704e..cbb2e32f 100644 --- a/po/el.po +++ b/po/el.po @@ -8,17 +8,17 @@ # Achilleas Pipinellis, 2013 # Achilleas Pipinellis, 2011 # Achilleas Pipinellis, 2012 -# Leonidas Spyropoulos, 2021 +# Leonidas Spyropoulos, 2021-2023 # Lukas Fleischer , 2011 # flamelab , 2011 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Greek (http://www.transifex.com/lfleischer/aurweb/language/el/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Leonidas Spyropoulos, 2021-2023\n" +"Language-Team: Greek (http://app.transifex.com/lfleischer/aurweb/language/el/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -35,7 +35,7 @@ msgstr "Μας συγχωρείτε, η σελίδα που ζητήσατε δ #: html/404.php template/pkgreq_close_form.php msgid "Note" -msgstr "" +msgstr "Σημείωση" #: html/404.php msgid "Git clone URLs are not meant to be opened in a browser." @@ -139,20 +139,20 @@ msgid "Type" msgstr "Είδος" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Προσθήκη ενός TU" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Αφαίρεση ενός TU" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Αφαίρεση ενός TU (αδήλωτη αδράνεια)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" -msgstr "Τροποποίηση των " +msgstr "Τροποποίηση των Bylaws" #: html/addvote.php template/tu_list.php msgid "Proposal" @@ -205,9 +205,10 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Καλωσήρθατε στο AUR! Διαβάστε παρακαλώ τον %sOδηγό Χρηστών του AUR%s και τον %sΟδηγό των Trusted Users%s για περισσότερες πληροφορίες. " +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -221,8 +222,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "Θυμηθείτε να ψηφίσετε τα αγαπημένα σας πακέτα!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Ορισμένα πακέτα μπορεί να μεταφερθούν ως binaries στο [community]." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Ορισμένα πακέτα μπορεί να μεταφερθούν ως binaries στο [extra]." #: html/home.php msgid "DISCLAIMER" @@ -271,7 +272,7 @@ msgstr "" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -314,9 +315,10 @@ msgstr "Συζήτηση" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -328,8 +330,8 @@ msgstr "Αναφορά Σφαλμάτων" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -530,8 +532,8 @@ msgid "Delete" msgstr "Διαγράψτε" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Μόνο οι Trusted Users και οι Developers μπορούν να διαγράψουν πακέτα." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -571,7 +573,7 @@ msgid "Disown" msgstr "Αποδέσμευση" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -661,8 +663,8 @@ msgid "Merge" msgstr "Συγχώνευση" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Μόνο οι Trusted Users και οι Developers μπορούν να συγχωνεύσουν πακέτα." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -719,8 +721,8 @@ msgid "I accept the terms and conditions above." msgstr "Αποδέχομαι τους παραπάνω όρους χρήσης." #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Trusted User" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -731,8 +733,8 @@ msgid "Voting is closed for this proposal." msgstr "Η ψηφοφορία έχει κλείσει για αυτή την πρόταση." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Μόνο οι Trusted Users έχουν δικαίωμα ψήφου." +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -1227,7 +1229,7 @@ msgstr "Developer" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1330,10 +1332,6 @@ msgstr "" msgid "Normal user" msgstr "Απλός χρήστης" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Αξιόπιστος χρήστης" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Ο Λογαριασμός έχει Ανασταλεί." @@ -1406,6 +1404,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1829,22 +1836,22 @@ msgstr "Συγχώνευση σε" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2101,8 +2108,8 @@ msgid "Registered Users" msgstr "Εγγεγραμμένοι Χρήστες" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Trusted Users" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2287,7 +2294,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2341,3 +2348,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/es.po b/po/es.po index ea7ac099..c723771c 100644 --- a/po/es.po +++ b/po/es.po @@ -4,30 +4,31 @@ # # Translators: # Adolfo Jayme-Barrientos, 2015 -# Angel Velasquez , 2011 +# Angel Velasquez , 2011,2023 +# Jose Serrano Pérez, 2023 # juantascon , 2011 # Lukas Fleischer , 2011 # neiko , 2011 # Nicolás de la Torre , 2012 -# prflr88 , 2012 -# prflr88 , 2016-2017 -# prflr88 , 2013-2016 -# prflr88 , 2016-2017 -# prflr88 , 2016 -# prflr88 , 2019 +# Pablo Lezaeta Reyes , 2012 +# Pablo Lezaeta Reyes , 2016-2017 +# Pablo Lezaeta Reyes , 2013-2016 +# Pablo Lezaeta Reyes , 2016-2017 +# Pablo Lezaeta Reyes , 2016 +# Pablo Lezaeta Reyes , 2019 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Spanish (http://www.transifex.com/lfleischer/aurweb/language/es/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Angel Velasquez , 2011,2023\n" +"Language-Team: Spanish (http://app.transifex.com/lfleischer/aurweb/language/es/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: es\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #: html/404.php msgid "Page Not Found" @@ -43,7 +44,7 @@ msgstr "Nota" #: html/404.php msgid "Git clone URLs are not meant to be opened in a browser." -msgstr "Las direcciones de clonado de Git no deberían ser habiertas en un navegador." +msgstr "Las direcciones de repositorios git no deberían ser abiertas en un navegador." #: html/404.php #, php-format @@ -86,7 +87,7 @@ msgstr "No tienes los permisos para editar esta cuenta." #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." -msgstr "" +msgstr "Contraseña inválida." #: html/account.php msgid "Use this form to search existing accounts." @@ -143,16 +144,16 @@ msgid "Type" msgstr "Tipo" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Agregar a un nuevo usuario de confianza" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Remover a un usuario de confianza" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Remover a un usuario de confianza (no declarado inactivo)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -209,9 +210,10 @@ msgstr "Buscar paquetes que soy coencargado" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "¡Bienvenido al repositorio de usuarios de Arch! Léase las %sDirectrices del usuario del AUR%s y las %sDirectrices del usuario de confianza del AUR%s para mayor información." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -225,8 +227,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "¡Recuerda votar tus paquetes favoritos!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Algunos paquetes pueden estar provistos de forma binaria en [community]." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Algunos paquetes pueden estar provistos de forma binaria en [extra]." #: html/home.php msgid "DISCLAIMER" @@ -275,8 +277,8 @@ msgstr "Solicitud de eliminación" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "Solicitar la eliminación de un paquete del repositorio de usuarios de Arch. No utilices esta opción si un paquete está roto pero puede ser arreglado fácilmente. En cambio, contacta al encargado del paquete y presenta una solicitud de orfandad si es necesario." +"the maintainer and file orphan request if necessary." +msgstr "" #: html/home.php msgid "Merge Request" @@ -318,10 +320,11 @@ msgstr "Debate" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "La discusión general sobre el repositorio de usuarios de Arch (AUR) y la estructura de usuarios de confianza se realiza en la lista de correos %saur-general%s. Para la discusión en relación con el desarrollo de la interfaz web del AUR, utiliza la lista de correo %saur-dev%s." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -332,9 +335,9 @@ msgstr "Informe de errores" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "Si encuentras un error en la interfaz web del AUR, llena un informe de error en nuestro %srastreador de errores o «bug tracker»%s. Usa este para reportar %súnicamente%s errores de la interfaz web del AUR. Para reportar errores de empaquetado debes contactar al encargado o dejar un comentario en la página respectiva del paquete." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "" #: html/home.php msgid "Package Search" @@ -383,7 +386,7 @@ msgstr "Proporciona tus datos de acceso" #: html/login.php msgid "User name or primary email address" -msgstr "" +msgstr "Nombre de usuario o dirección de correo electrónico principal" #: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" @@ -534,8 +537,8 @@ msgid "Delete" msgstr "Eliminar" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Solamente usuarios de confianza y desarrolladores pueden eliminar paquetes." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -575,8 +578,8 @@ msgid "Disown" msgstr "Abandonar" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "Solamente usuarios de confianza y desarrolladores puede forzar el abandono de paquetes." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -665,8 +668,8 @@ msgid "Merge" msgstr "Unión" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Solamente usuarios de confianza y desarrolladores pueden unir paquetes." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -723,8 +726,8 @@ msgid "I accept the terms and conditions above." msgstr "Acepto los términos y condiciones anteriores." #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Usuario de confianza" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -735,8 +738,8 @@ msgid "Voting is closed for this proposal." msgstr "Las votaciones para esta propuesta están cerradas." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Solamente usuarios de confianza pueden votar." +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -1106,12 +1109,12 @@ msgstr "No se pudo añadir a la lista de notificaciones." #: lib/pkgbasefuncs.inc.php #, php-format msgid "You have been added to the comment notification list for %s." -msgstr "Haz sido añadido a la lista de notificaciones de comentarios de %s." +msgstr "Ha sido añadido a la lista de notificaciones de comentarios de %s." #: lib/pkgbasefuncs.inc.php #, php-format msgid "You have been removed from the comment notification list for %s." -msgstr "Haz sido eliminado de la lista de notificaciones de comentarios de %s." +msgstr "Ha sido eliminado de la lista de notificaciones de comentarios de %s." #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to undelete this comment." @@ -1231,8 +1234,8 @@ msgstr "Desarrollador" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "Usuarios de confianza y desarrolladores" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1334,10 +1337,6 @@ msgstr "Tu nombre de usuario es el nombre que usarás para iniciar sesión. Es v msgid "Normal user" msgstr "Usuario normal" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Usuario de confianza" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Cuenta suspendida" @@ -1410,6 +1409,15 @@ msgid "" " the Arch User Repository." msgstr "La siguiente información únicamente es necesaria si deseas subir paquetes al repositorio de usuarios de Arch." +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "Clave pública SSH" @@ -1590,6 +1598,7 @@ msgid "%d pending request" msgid_plural "%d pending requests" msgstr[0] "Hay %d solicitud pendiente" msgstr[1] "Hay %d solicitudes pendientes" +msgstr[2] "Hay %d solicitudes pendientes" #: template/pkgbase_actions.php msgid "Adopt Package" @@ -1833,26 +1842,26 @@ msgstr "Unir en" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "Al enviar una solicitud de eliminación, le preguntas a un usuario de confianza que elimine el paquete base. Este tipo de solicitud debe ser utilizado para los duplicados, programas abandonados por el desarrollador principal o encargado, así como programas ilegales e irreparablemente rotos." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "Al enviar una solicitud de unión, le preguntas a un usuario de confianza que elimine el paquete base y transfiera sus votos y comentarios a otro paquete base. La unión de un paquete no afecta a los correspondientes repositorios Git. Por tanto asegúrate de actualizar el historia Git del paquete de destino tú mismo." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "Al enviar una solicitud de orfandad, le preguntas a un usuario de confianza que remueva la propiedad sobre el paquete base al encargado principal de este. Por favor, haz esto solamente si el paquete necesita una acción de mantenención, el encargado no presenta signos de actividad y ya intentaste ponerte en contacto con él anteriormente." +msgstr "" #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -1864,6 +1873,7 @@ msgid "%d package request found." msgid_plural "%d package requests found." msgstr[0] "Se encontró %d solicitud para el paquete." msgstr[1] "Se encontraron %d solicitudes para el paquete." +msgstr[2] "Se encontraron %d solicitudes para el paquete." #: template/pkgreq_results.php template/pkg_search_results.php #, php-format @@ -1888,6 +1898,7 @@ msgid "~%d day left" msgid_plural "~%d days left" msgstr[0] "~%d día restante" msgstr[1] "~%d días restantes" +msgstr[2] "~%d días restantes" #: template/pkgreq_results.php #, php-format @@ -1895,6 +1906,7 @@ msgid "~%d hour left" msgid_plural "~%d hours left" msgstr[0] "Aprox. %d hora restante" msgstr[1] "Aprox. %d horas restantes" +msgstr[2] "Aprox. %d horas restantes" #: template/pkgreq_results.php msgid "<1 hour left" @@ -2023,6 +2035,7 @@ msgid "%d package found." msgid_plural "%d packages found." msgstr[0] "%d paquete fue encontrado." msgstr[1] "%d paquetes fueron encontrados." +msgstr[2] "%d paquetes fueron encontrados." #: template/pkg_search_results.php msgid "Version" @@ -2105,8 +2118,8 @@ msgid "Registered Users" msgstr "Usuarios registrados" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Usuarios de confianza" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2291,7 +2304,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2345,3 +2358,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/es_419.po b/po/es_419.po index 444eccb7..f1997606 100644 --- a/po/es_419.po +++ b/po/es_419.po @@ -8,24 +8,25 @@ # Lukas Fleischer , 2011 # neiko , 2011 # Nicolás de la Torre , 2012 -# prflr88 , 2016-2017 -# prflr88 , 2012,2015-2016 -# prflr88 , 2016-2017 -# prflr88 , 2016 -# prflr88 , 2019 +# Oliver Hattshire , 2021 +# Pablo Lezaeta Reyes , 2016-2017 +# Pablo Lezaeta Reyes , 2012,2015-2016 +# Pablo Lezaeta Reyes , 2016-2017 +# Pablo Lezaeta Reyes , 2016 +# Pablo Lezaeta Reyes , 2019,2022 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2020-01-31 08:29+0000\n" -"Last-Translator: Lukas Fleischer\n" -"Language-Team: Spanish (Latin America) (http://www.transifex.com/lfleischer/aurweb/language/es_419/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Pablo Lezaeta Reyes , 2019,2022\n" +"Language-Team: Spanish (Latin America) (http://app.transifex.com/lfleischer/aurweb/language/es_419/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: es_419\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #: html/404.php msgid "Page Not Found" @@ -41,7 +42,7 @@ msgstr "Nota" #: html/404.php msgid "Git clone URLs are not meant to be opened in a browser." -msgstr "Las direcciones de clonado de Git no deberían ser habiertas en un navegador." +msgstr "Las direcciones de clonado de Git no deberían ser abiertas en un navegador." #: html/404.php #, php-format @@ -60,7 +61,7 @@ msgstr "Servicio no disponible" #: html/503.php msgid "" "Don't panic! This site is down due to maintenance. We will be back soon." -msgstr "¡No se asustes! El sitio está desactivado por mantenimiento. Pronto volveremos." +msgstr "¡No se asuste! El sitio está desactivado por mantenimiento. Pronto volveremos." #: html/account.php msgid "Account" @@ -76,7 +77,7 @@ msgstr "No está autorizado a acceder a esta área." #: html/account.php msgid "Could not retrieve information for the specified user." -msgstr "No se pudo obtener la información del usuario especificado." +msgstr "No se pudo recuperar la información del usuario especificado." #: html/account.php msgid "You do not have permission to edit this account." @@ -84,7 +85,7 @@ msgstr "No tiene permisos para editar esta cuenta." #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." -msgstr "" +msgstr "Contraseña no válida." #: html/account.php msgid "Use this form to search existing accounts." @@ -100,7 +101,7 @@ msgstr "Añadir propuesta" #: html/addvote.php msgid "Invalid token for user action." -msgstr "Elemento inválido para la acción del usuario." +msgstr "Elemento no válido para la acción del usuario." #: html/addvote.php msgid "Username does not exist." @@ -129,7 +130,7 @@ msgstr "Envíe una propuesta a la cual votar." #: html/addvote.php msgid "Applicant/TU" -msgstr "Candidato/Usuario de confianza (UC)" +msgstr "Candidato/Usuario de confianza" #: html/addvote.php msgid "(empty if not applicable)" @@ -141,16 +142,16 @@ msgid "Type" msgstr "Tipo" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Agregar a un nuevo Usuario de Confianza" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Remover a un Usuario de Confianza" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Remover a un Usuario de Confianza (no declarado inactivo)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -186,7 +187,7 @@ msgstr "Mis paquetes marcados" #: html/home.php msgid "My Requests" -msgstr "Mis peticiones" +msgstr "Mis Solicitudes" #: html/home.php msgid "My Packages" @@ -207,9 +208,10 @@ msgstr "Buscar paquetes que soy coencargado" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "¡Bienvenido al repositorio de usuarios de Arch! Lea la %sGuía del usuario del AUR%s y la %sGuía del usuario de Confianza del AUR%s para mayor información." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -223,8 +225,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "¡Recuerde votar sus paquetes favoritos!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Algunos paquetes pueden estar disponibles de forma binaria en [community]." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Algunos paquetes pueden estar disponibles de forma binaria en [extra]." #: html/home.php msgid "DISCLAIMER" @@ -257,13 +259,13 @@ msgstr "Existen tres tipos de peticiones que pueden presentarse en el recuadro % #: html/home.php msgid "Orphan Request" -msgstr "Petición de Orfandad" +msgstr "Solicitud de Abandono" #: html/home.php msgid "" "Request a package to be disowned, e.g. when the maintainer is inactive and " "the package has been flagged out-of-date for a long time." -msgstr "Pedir la orfandad de un paquete, por ejemplo, cuando el encargado está inactivo y el paquete fue marcado como desactualizado por un largo tiempo." +msgstr "Pedir el abandono de un paquete, por ejemplo, cuando el encargado está inactivo y el paquete fue marcado como desactualizado por un largo tiempo." #: html/home.php msgid "Deletion Request" @@ -273,8 +275,8 @@ msgstr "Petición de Borrado" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "Pedir que un paquete sea borrado del Repositorio Usuarios de Arch. Por favor, no use esta opción si un paquete está roto y se puede arreglar fácilmente. En cambio, contacte con el encargado del paquete y presentar solicitud orfandad si es necesario." +"the maintainer and file orphan request if necessary." +msgstr "" #: html/home.php msgid "Merge Request" @@ -284,14 +286,14 @@ msgstr "Petición de Fusión" msgid "" "Request a package to be merged into another one. Can be used when a package " "needs to be renamed or replaced by a split package." -msgstr "Pedir que se fusione un paquete en otro. Puede usarla cuando un paquete tiene que ser cambiado de nombre o sustituido por un paquete dividido." +msgstr "Solicitar que se fusione un paquete en otro. Puede usarla cuando un paquete tiene que ser cambiado de nombre o sustituido por un paquete dividido." #: html/home.php #, php-format msgid "" "If you want to discuss a request, you can use the %saur-requests%s mailing " "list. However, please do not use that list to file requests." -msgstr "Si quiere discutir una petición, puede usar la lista de correo %saur-peticiones%s. Sin embargo, por favor no utilice esa lista para presentar solicitudes." +msgstr "Si quiere discutir una solicitud, puede usar la lista de correo %saur-requests%s. Sin embargo, por favor no utilice esa lista para presentar solicitudes." #: html/home.php msgid "Submitting Packages" @@ -316,10 +318,11 @@ msgstr "Debate" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "La discusión general acerca del Repositorio de Usuarios de Arch (AUR) y la estructura de Usuarios de Confianza se realiza en la lista de correos %saur-general%s. Para discusiones relacionadas con el desarrollo de la interfaz web del AUR, utilice la lista de correo %saur-dev%s." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -330,9 +333,9 @@ msgstr "Informe de errores" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "Si encuentra un error en la interfaz web del AUR, llene un informe de fallo en nuestro %s«bug tracker»%s. Use este para reportar %súnicamente%s errores de la interfaz web del AUR. Para reportar errores de empaquetado debe contactar con el encargado o dejar un comentario en la página respectiva del paquete." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "" #: html/home.php msgid "Package Search" @@ -381,7 +384,7 @@ msgstr "Introduce las credenciales de autentificación" #: html/login.php msgid "User name or primary email address" -msgstr "" +msgstr "Nombre de usuario o dirección de correo electrónico principal" #: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" @@ -445,7 +448,7 @@ msgstr "Su contraseña fue reiniciada con éxito." #: html/passreset.php msgid "Confirm your user name or primary e-mail address:" -msgstr "" +msgstr "Confirma tu nombre de usuario o dirección de correo electrónico principal:" #: html/passreset.php msgid "Enter your new password:" @@ -464,11 +467,11 @@ msgstr "Continuar" msgid "" "If you have forgotten the user name and the primary e-mail address you used " "to register, please send a message to the %saur-general%s mailing list." -msgstr "" +msgstr "Si has olvidado el nombre de usuario y la dirección de correo electrónico principal usados al registrarte, por favor envía un mensaje a la lista de correo %saur-general%s." #: html/passreset.php msgid "Enter your user name or your primary e-mail address:" -msgstr "" +msgstr "Ingresa tu nombre de usuario o tu dirección de correo electrónico principal:" #: html/pkgbase.php msgid "Package Bases" @@ -480,6 +483,12 @@ msgid "" "checkbox." msgstr "Los paquetes seleccionados no fueron abandonados, marque la casilla de confirmación." +#: aurweb/routers/packages.py +msgid "" +"The selected packages have not been adopted, check the confirmation " +"checkbox." +msgstr "Los paquetes seleccionados no han sido adoptados, marque la casilla de verificación." + #: html/pkgbase.php lib/pkgreqfuncs.inc.php msgid "Cannot find package to merge votes and comments into." msgstr "No se puede encontrar el paquete para fusionar sus votos y comentarios." @@ -526,8 +535,8 @@ msgid "Delete" msgstr "Borrar" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Solo Usuarios de Confianza y Desarrolladores pueden borrar paquetes." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -544,7 +553,7 @@ msgstr "Use este formulario para abandonar el paquete base %s %s %s que incluye msgid "" "By selecting the checkbox, you confirm that you want to no longer be a " "package co-maintainer." -msgstr "" +msgstr "Al seleccionar la casilla de verificación, confirmas que ya no quieres ser co-mantenedor del paquete." #: html/pkgdisown.php #, php-format @@ -567,8 +576,8 @@ msgid "Disown" msgstr "Abandonar" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "Solo Usuarios de Confianza y Desarrolladores pueden forzar el abandono de paquetes." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -578,6 +587,14 @@ msgstr "Marcar comentario" msgid "Flag Package Out-Of-Date" msgstr "Marcado como desactualizado" +#: templates/packages/flag.html +msgid "" +"This seems to be a VCS package. Please do %snot%s flag it out-of-date if the" +" package version in the AUR does not match the most recent commit. Flagging " +"this package should only be done if the sources moved or changes in the " +"PKGBUILD are required because of recent upstream changes." +msgstr "Esto parece ser un paquete VCS. 1%sNo1%s lo marque como obsoleto si la versión del paquete en el AUR no coincide con la commit más reciente. Solo se debe marcar este paquete si las fuentes se movieron o si se requiere cambios en el PKGBUILD debido a cambios recientes en las fuentes." + #: html/pkgflag.php #, php-format msgid "" @@ -649,16 +666,16 @@ msgid "Merge" msgstr "Fusión" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Solo Usuarios de Confianza y Desarrolladores pueden fusionar paquetes." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" -msgstr "Enviar petición" +msgstr "Enviar Solicitud" #: html/pkgreq.php template/pkgreq_close_form.php msgid "Close Request" -msgstr "Cerrar Petición" +msgstr "Cerrar Solicitud" #: html/pkgreq.php lib/aur.inc.php lib/pkgfuncs.inc.php msgid "First" @@ -678,7 +695,7 @@ msgstr "Último" #: html/pkgreq.php template/header.php msgid "Requests" -msgstr "Petición" +msgstr "Solicitudes" #: html/register.php template/header.php msgid "Register" @@ -707,8 +724,8 @@ msgid "I accept the terms and conditions above." msgstr "Acepto las Terminos y condiciones anteriores." #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Usuario de Confianza" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -719,8 +736,8 @@ msgid "Voting is closed for this proposal." msgstr "Las votaciones para esta propuesta están cerradas." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Solo Usuarios de Confianza pueden votar." +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -775,7 +792,7 @@ msgstr "Solo puede contener un punto, guion bajo o guion." #: lib/acctfuncs.inc.php msgid "Please confirm your new password." -msgstr "" +msgstr "Por favor confirma tu nueva contraseña." #: lib/acctfuncs.inc.php msgid "The email address is invalid." @@ -783,7 +800,7 @@ msgstr "La dirección de correo no es válida." #: lib/acctfuncs.inc.php msgid "The backup email address is invalid." -msgstr "" +msgstr "La dirección de correo electrónico de respaldo no es válida." #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." @@ -826,15 +843,15 @@ msgstr "La clave pública SSH %s%s%s ya está en uso." #: lib/acctfuncs.inc.php msgid "The CAPTCHA is missing." -msgstr "" +msgstr "Falta el CAPTCHA." #: lib/acctfuncs.inc.php msgid "This CAPTCHA has expired. Please try again." -msgstr "" +msgstr "El CAPTCHA expiró. Por favor intenta de nuevo." #: lib/acctfuncs.inc.php msgid "The entered CAPTCHA answer is invalid." -msgstr "" +msgstr "El CAPTCHA ingresado no es válido." #: lib/acctfuncs.inc.php #, php-format @@ -874,6 +891,10 @@ msgstr "El formulario de registro ha sido deshabilitado para su dirección IP, p msgid "Account suspended" msgstr "Cuenta suspendida" +#: aurweb/routers/accounts.py +msgid "You do not have permission to suspend accounts." +msgstr "No tiene los permiso para suspender cuentas." + #: lib/acctfuncs.inc.php #, php-format msgid "" @@ -888,7 +909,7 @@ msgstr "Contraseña o nombre de usuario erróneos." #: lib/acctfuncs.inc.php msgid "An error occurred trying to generate a user session." -msgstr "Un error ocurrió intentando generar la sesión." +msgstr "Un error ocurrió intentando generar la sesión de usuario." #: lib/acctfuncs.inc.php msgid "Invalid e-mail and reset key combination." @@ -959,6 +980,30 @@ msgstr "Error al recuperar los detalles del paquete." msgid "Package details could not be found." msgstr "Los detalles del paquete no se pudieron encontrar." +#: aurweb/routers/auth.py +msgid "Bad Referer header." +msgstr "Encabezado de referencia no correcto." + +#: aurweb/routers/packages.py +msgid "You did not select any packages to be notified about." +msgstr "No seleccionó ningún paquete para recibir notificaciones." + +#: aurweb/routers/packages.py +msgid "The selected packages' notifications have been enabled." +msgstr "Se han habilitado las notificaciones de los paquetes seleccionados." + +#: aurweb/routers/packages.py +msgid "You did not select any packages for notification removal." +msgstr "No seleccionó ningún paquete para la eliminación de notificaciones" + +#: aurweb/routers/packages.py +msgid "A package you selected does not have notifications enabled." +msgstr "Un paquete que seleccionó no tiene notificaciones habilitadas." + +#: aurweb/routers/packages.py +msgid "The selected packages' notifications have been removed." +msgstr "Se han eliminado las notificaciones de los paquetes seleccionados." + #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can flag packages." msgstr "Debe autentificarse antes de poder marcar paquetes." @@ -995,6 +1040,10 @@ msgstr "No posee los permisos para borrar paquetes." msgid "You did not select any packages to delete." msgstr "No seleccionó ningún paquete para borrar." +#: aurweb/routers/packages.py +msgid "One of the packages you selected does not exist." +msgstr "Uno de los paquetes que seleccionó no existe." + #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been deleted." msgstr "Los paquetes seleccionados se han borrado." @@ -1003,10 +1052,18 @@ msgstr "Los paquetes seleccionados se han borrado." msgid "You must be logged in before you can adopt packages." msgstr "Debe autentificarse antes de poder adoptar paquetes." +#: aurweb/routers/package.py +msgid "You are not allowed to adopt one of the packages you selected." +msgstr "No puede adoptar uno de los paquetes que seleccionó." + #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can disown packages." msgstr "Debe autentificarse antes de poder abandonar paquetes." +#: aurweb/routers/packages.py +msgid "You are not allowed to disown one of the packages you selected." +msgstr "No puede abandonar uno de los paquetes que seleccionó." + #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to adopt." msgstr "No seleccionó ningún paquete para adoptar." @@ -1175,8 +1232,8 @@ msgstr "Desarrollador" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "Usuarios de Confianza y desarrolladores" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1247,7 +1304,7 @@ msgstr "Editar la cuenta de este usuario" #: template/account_details.php msgid "List this user's comments" -msgstr "" +msgstr "Mostrar los comentarios de este usuario" #: template/account_edit_form.php #, php-format @@ -1262,7 +1319,7 @@ msgstr "Haga clic %saquí%s para ver los detalles del usuario." #: template/account_edit_form.php #, php-format msgid "Click %shere%s to list the comments made by this account." -msgstr "" +msgstr "Haz clic %saquí%s para mostrar los comentarios hechos por esta cuenta." #: template/account_edit_form.php msgid "required" @@ -1278,10 +1335,6 @@ msgstr "Su nombre de usuario es el nombre que usará para iniciar sesión. Es vi msgid "Normal user" msgstr "Usuario normal" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Usuario de Confianza" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Cuenta suspendida" @@ -1305,30 +1358,30 @@ msgid "" "If you do not hide your email address, it is visible to all registered AUR " "users. If you hide your email address, it is visible to members of the Arch " "Linux staff only." -msgstr "" +msgstr "Si no esconde su dirección de correo electrónico, esta será visible para todo usuario registrado en el AUR. Si esconde su dirección de correo electrónico, esta será visible sólo por el equipo de Arch Linux." #: template/account_edit_form.php msgid "Backup Email Address" -msgstr "" +msgstr "Dirección de correo electrónico de respaldo" #: template/account_edit_form.php msgid "" "Optionally provide a secondary email address that can be used to restore " "your account in case you lose access to your primary email address." -msgstr "" +msgstr "Opcionalmente proporciona una dirección de correo electrónico secundaria para poder restaurar tu cuenta en caso de que pierdas acceso tu dirección principal." #: template/account_edit_form.php msgid "" "Password reset links are always sent to both your primary and your backup " "email address." -msgstr "" +msgstr "Los enlaces de restauración de contraseña siempre se envían a tus direcciones de correo electrónico primaria y de respaldo." #: template/account_edit_form.php #, php-format msgid "" "Your backup email address is always only visible to members of the Arch " "Linux staff, independent of the %s setting." -msgstr "" +msgstr "Tu dirección de correo de respaldo siempre es sólo visible por el equipo de Arch Linux, sin importar lo seleccionado en la configuración %s." #: template/account_edit_form.php msgid "Language" @@ -1342,7 +1395,7 @@ msgstr "Zona horaria" msgid "" "If you want to change the password, enter a new password and confirm the new" " password by entering it again." -msgstr "" +msgstr "Si quieres cambiar la contraseña, ingresa una nueva y confírmala en el cuadro correspondiente." #: template/account_edit_form.php msgid "Re-type password" @@ -1354,6 +1407,15 @@ msgid "" " the Arch User Repository." msgstr "La siguiente información es necesaria únicamente si quiere subir paquetes al Repositorio de Usuarios de Arch." +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "Clave pública SSH" @@ -1376,21 +1438,21 @@ msgstr "Notificarme de cambios de propietario" #: template/account_edit_form.php msgid "To confirm the profile changes, please enter your current password:" -msgstr "" +msgstr "Para confirmar los cambios a tu perfil, por favor ingresa tu contraseña:" #: template/account_edit_form.php msgid "Your current password" -msgstr "" +msgstr "Tu contraseña actual" #: template/account_edit_form.php msgid "" "To protect the AUR against automated account creation, we kindly ask you to " "provide the output of the following command:" -msgstr "" +msgstr "Para proteger el AUR contra creaciones de cuentas automatizados, te pedimos amablemente que ingreses la salida del siguiente comando:" #: template/account_edit_form.php msgid "Answer" -msgstr "" +msgstr "Respuesta" #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php @@ -1534,6 +1596,7 @@ msgid "%d pending request" msgid_plural "%d pending requests" msgstr[0] "Hay %d petición pendiente" msgstr[1] "Hay %d peticiones pendientes" +msgstr[2] "Hay %d peticiones pendientes" #: template/pkgbase_actions.php msgid "Adopt Package" @@ -1553,7 +1616,7 @@ msgstr "Solo lectura" #: template/pkgbase_details.php template/pkg_details.php msgid "click to copy" -msgstr "" +msgstr "haz clic para copiar" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php @@ -1605,12 +1668,12 @@ msgstr "Agregar un comentario" msgid "" "Git commit identifiers referencing commits in the AUR package repository and" " URLs are converted to links automatically." -msgstr "" +msgstr "Los identificadores de commits de Git que referencian URLs y commits del repositorio de paquetes del AUR son convertidos a enlaces automáticamente." #: template/pkg_comment_form.php #, php-format msgid "%sMarkdown syntax%s is partially supported." -msgstr "" +msgstr "La %ssintaxis Markdown%s está parcialmente soportada." #: template/pkg_comments.php msgid "Pinned Comments" @@ -1622,7 +1685,7 @@ msgstr "Últimos comentarios" #: template/pkg_comments.php msgid "Comments for" -msgstr "" +msgstr "Comentarios para" #: template/pkg_comments.php #, php-format @@ -1637,7 +1700,7 @@ msgstr "Comentario anónimo en %s" #: template/pkg_comments.php #, php-format msgid "Commented on package %s on %s" -msgstr "" +msgstr "Comentó en el paquete %s el %s." #: template/pkg_comments.php #, php-format @@ -1769,7 +1832,7 @@ msgstr "Borrado" #: template/pkgreq_form.php msgid "Orphan" -msgstr "Orfandad" +msgstr "Abandono" #: template/pkgreq_form.php template/pkg_search_results.php msgid "Merge into" @@ -1777,30 +1840,30 @@ msgstr "Fusionar en" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "Al enviar una Petición de Borrado, le preguntará a un Usuario de Confianza que elimine dicho paquete base. Este tipo de peticiones debe ser utilizada para duplicados, programas abandonados por el desarrollador principal o encargado, así como programas ilegales e irreparablemente rotos." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "Al enviar una Petición de Fusión, le preguntará a un Usuario de Confianza que borre el paquete base y transfiera sus votos y comentarios a otro paquete base. La fusión de un paquete no afecta a los correspondientes repositorios Git. Por lo tanto asegúrese de actualizar el historia Git del paquete de destino uste mismo." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "Al enviar una Petición de Orfandad, le preguntarás a un Usuario de Confianza que le quite la propiedad sobre el paquete base al encargado principal de este. Por favor, haga esto solo si el paquete necesita acciones de mantenención para funcionar, el encargado no presenta da de actividad y ya intentó ponerse en contacto con él anteriormente." +msgstr "" #: template/pkgreq_results.php msgid "No requests matched your search criteria." -msgstr "Ninguna peticiones coincide con su criterio de búsqueda." +msgstr "Ninguna solicitud coincide con su criterio de búsqueda." #: template/pkgreq_results.php #, php-format @@ -1808,6 +1871,7 @@ msgid "%d package request found." msgid_plural "%d package requests found." msgstr[0] "Se encontró %d solicitud para el paquete." msgstr[1] "Se encontraron %d solicitudes para el paquete." +msgstr[2] "Se encontraron %d solicitudes para el paquete." #: template/pkgreq_results.php template/pkg_search_results.php #, php-format @@ -1832,6 +1896,7 @@ msgid "~%d day left" msgid_plural "~%d days left" msgstr[0] "~%d día restante" msgstr[1] "~%d días restantes" +msgstr[2] "~%d días restantes" #: template/pkgreq_results.php #, php-format @@ -1839,6 +1904,7 @@ msgid "~%d hour left" msgid_plural "~%d hours left" msgstr[0] "Aprox. %d hora restante" msgstr[1] "Aprox. %d horas restantes" +msgstr[2] "Aprox. %d horas restantes" #: template/pkgreq_results.php msgid "<1 hour left" @@ -1951,7 +2017,7 @@ msgstr "Ir" #: template/pkg_search_form.php msgid "Orphans" -msgstr "Huérfanos" +msgstr "Abandonados" #: template/pkg_search_results.php msgid "Error retrieving package list." @@ -1967,6 +2033,7 @@ msgid "%d package found." msgid_plural "%d packages found." msgstr[0] "%d paquete fue encontrado." msgstr[1] "%d paquetes fueron encontrados." +msgstr[2] "%d paquetes fueron encontrados." #: template/pkg_search_results.php msgid "Version" @@ -1986,7 +2053,7 @@ msgstr "Sí" #: template/pkg_search_results.php msgid "orphan" -msgstr "huérfano" +msgstr "abandonado" #: template/pkg_search_results.php msgid "Actions" @@ -2026,7 +2093,7 @@ msgstr "Estadísticas" #: template/stats/general_stats_table.php msgid "Orphan Packages" -msgstr "Paquetes huérfanos" +msgstr "Paquetes Abandonados" #: template/stats/general_stats_table.php msgid "Packages added in the past 7 days" @@ -2049,8 +2116,8 @@ msgid "Registered Users" msgstr "Usuarios registrados" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Usuarios de Confianza" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2103,7 +2170,7 @@ msgstr "Participación" #: template/tu_last_votes_list.php msgid "Last Votes by TU" -msgstr "Último voto del Usuario de Confianza" +msgstr "Últimos votos del Usuario de Confianza" #: template/tu_last_votes_list.php msgid "Last vote" @@ -2131,7 +2198,7 @@ msgid "" "A password reset request was submitted for the account {user} associated " "with your email address. If you wish to reset your password follow the link " "[1] below, otherwise ignore this message and nothing will happen." -msgstr "" +msgstr "Una solicitud de reinicio de contraseña fue hecha para la cuenta {user} asociada con tu dirección de correo electrónico. Si deseas reiniciar tu contraseña, sigue el enlace [1] debajo, de lo contrario ignora este mensaje y nada pasará." #: scripts/notify.py msgid "Welcome to the Arch User Repository" @@ -2142,7 +2209,7 @@ msgid "" "Welcome to the Arch User Repository! In order to set an initial password for" " your new account, please click the link [1] below. If the link does not " "work, try copying and pasting it into your browser." -msgstr "" +msgstr "¡Le damos la bienvenida al Repositorio Usuarios de Arch! Para poder configurar su contraseña inicial, por favor haga clic en el enlace [1] de abajo. Si el enlace no funciona, pruebe copiando y pegándolo en su navegador." #: scripts/notify.py #, python-brace-format @@ -2159,62 +2226,62 @@ msgstr "el usuario {user} [1] agregó el siguiente comentario al paquete base {p msgid "" "If you no longer wish to receive notifications about this package, please go" " to the package page [2] and select \"{label}\"." -msgstr "" +msgstr "Si ya no deseas recibir notificaciones sobre este paquete, por favor vé a la página del paquete [2] y selecciona \"{label}\"." #: scripts/notify.py #, python-brace-format msgid "AUR Package Update: {pkgbase}" -msgstr "" +msgstr "Actualización de paquete en el AUR: {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "{user} [1] pushed a new commit to {pkgbase} [2]." -msgstr "" +msgstr "{user} [1] añadió un nuevo commit a {pkgbase} [2]." #: scripts/notify.py #, python-brace-format msgid "AUR Out-of-date Notification for {pkgbase}" -msgstr "" +msgstr "Notificación en el AUR de paquete obsoleto para {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" -msgstr "" +msgstr "Tu paquete {pkgbase} [1] ha sido marcado como desactualizado por {user} [2]:" #: scripts/notify.py #, python-brace-format msgid "AUR Ownership Notification for {pkgbase}" -msgstr "" +msgstr "Notificación en el AUR de propiedad para {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was adopted by {user} [2]." -msgstr "" +msgstr "El paquete {pkgbase} [1] fué adoptado por {user} [2]." #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was disowned by {user} [2]." -msgstr "" +msgstr "El paquete {pkgbase} [1] fué abandonado por {user} [2]." #: scripts/notify.py #, python-brace-format msgid "AUR Co-Maintainer Notification for {pkgbase}" -msgstr "" +msgstr "Notificación en el AUR de Coencargado para {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "You were added to the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "Te han añadido a la lista de coencargados de {pkgbase} [1]." #: scripts/notify.py #, python-brace-format msgid "You were removed from the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "Te han eliminado de la lista de coencargados de {pkgbase} [1]." #: scripts/notify.py #, python-brace-format msgid "AUR Package deleted: {pkgbase}" -msgstr "" +msgstr "Paquete en el AUR elimnado: {pkgbase}" #: scripts/notify.py #, python-brace-format @@ -2223,7 +2290,7 @@ msgid "" "\n" "-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." -msgstr "" +msgstr "{user} [1] fusionó {old} [2] en {new} [3].\n\n-- \nSi ya no deseas recibir notificaciones sobre el nuevo paquete, por favor dirigete a [3] y haz clic en \"{label}\"." #: scripts/notify.py #, python-brace-format @@ -2231,11 +2298,11 @@ msgid "" "{user} [1] deleted {pkgbase} [2].\n" "\n" "You will no longer receive notifications about this package." -msgstr "" +msgstr "{user} [1] eliminó {pkgbase} [2].\n\nYo no recibirás actualizaciones de este paquete." #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2243,4 +2310,81 @@ msgstr "" msgid "" "Please remember to cast your vote on proposal {id} [1]. The voting period " "ends in less than 48 hours." +msgstr "Por favor recuerda efectuar tu voto en la propuesta {id} [1]. El lapso para votar termina en menos de 48 horas." + +#: aurweb/routers/accounts.py +msgid "Invalid account type provided." +msgstr "Se proporcionó un tipo de cuenta no válido." + +#: aurweb/routers/accounts.py +msgid "You do not have permission to change account types." +msgstr "No tiene permisos para cambiar los tipos de cuenta." + +#: aurweb/routers/accounts.py +msgid "You do not have permission to change this user's account type to %s." +msgstr "No tiene permisos para cambiar el tipo de cuenta de este usuario a 1%s." + +#: aurweb/packages/requests.py +msgid "No due existing orphan requests to accept for %s." +msgstr "No existen solicitudes huérfanas pendientes para aceptar de 1%s." + +#: aurweb/asgi.py +msgid "Internal Server Error" +msgstr "Error interno del servidor" + +#: templates/errors/500.html +msgid "A fatal error has occurred." +msgstr "Ha ocurrido un error fatal." + +#: templates/errors/500.html +msgid "" +"Details have been logged and will be reviewed by the postmaster posthaste. " +"We apologize for any inconvenience this may have caused." +msgstr "Los detalles han sido registrados y serán revisados ​​por el administrador de correos inmediatamente. Pedimos disculpas por cualquier inconveniente que esto pueda haber causado." + +#: aurweb/scripts/notify.py +msgid "AUR Server Error" +msgstr "Error del servidor del AUR" + +#: templates/pkgbase/merge.html templates/packages/delete.html +#: templates/packages/disown.html +msgid "Related package request closure comments..." +msgstr "Comentarios de cierre de solicitud de paquete relacionados..." + +#: templates/pkgbase/merge.html templates/packages/delete.html +msgid "" +"This action will close any pending package requests related to it. If " +"%sComments%s are omitted, a closure comment will be autogenerated." +msgstr "Esta acción cerrará cualquier solicitud de paquete pendiente relacionada con este. Si se omiten 1%sComentarios1%s, se generará automáticamente un comentario de cierre." + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." msgstr "" diff --git a/po/et.po b/po/et.po index 9b6493b5..e25c7eb2 100644 --- a/po/et.po +++ b/po/et.po @@ -6,11 +6,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Estonian (http://www.transifex.com/lfleischer/aurweb/language/et/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Estonian (http://app.transifex.com/lfleischer/aurweb/language/et/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -131,15 +131,15 @@ msgid "Type" msgstr "" #: html/addvote.php -msgid "Addition of a TU" +msgid "Addition of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU" +msgid "Removal of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" msgstr "" #: html/addvote.php @@ -197,8 +197,9 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." msgstr "" #: html/home.php @@ -213,7 +214,7 @@ msgid "Remember to vote for your favourite packages!" msgstr "" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." +msgid "Some packages may be provided as binaries in [extra]." msgstr "" #: html/home.php @@ -263,7 +264,7 @@ msgstr "" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -306,9 +307,10 @@ msgstr "" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -320,8 +322,8 @@ msgstr "" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -522,7 +524,7 @@ msgid "Delete" msgstr "" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." +msgid "Only Package Maintainers and Developers can delete packages." msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php @@ -563,7 +565,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -653,7 +655,7 @@ msgid "Merge" msgstr "" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." +msgid "Only Package Maintainers and Developers can merge packages." msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php @@ -711,7 +713,7 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" +msgid "Package Maintainer" msgstr "" #: html/tu.php @@ -723,7 +725,7 @@ msgid "Voting is closed for this proposal." msgstr "" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -1219,7 +1221,7 @@ msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1322,10 +1324,6 @@ msgstr "" msgid "Normal user" msgstr "" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "" @@ -1398,6 +1396,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1821,22 +1828,22 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2093,7 +2100,7 @@ msgid "Registered Users" msgstr "" #: template/stats/general_stats_table.php -msgid "Trusted Users" +msgid "Package Maintainers" msgstr "" #: template/stats/updates_table.php @@ -2279,7 +2286,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2333,3 +2340,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/fi.po b/po/fi.po index 39cfe626..eb97a47d 100644 --- a/po/fi.po +++ b/po/fi.po @@ -10,11 +10,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Finnish (http://www.transifex.com/lfleischer/aurweb/language/fi/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Nikolay Korotkiy , 2018-2019\n" +"Language-Team: Finnish (http://app.transifex.com/lfleischer/aurweb/language/fi/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -135,15 +135,15 @@ msgid "Type" msgstr "Tyyppi" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Luotetun käyttäjän (TU) lisääminen" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Luotetun käyttäjän (TU) poisto" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" msgstr "" #: html/addvote.php @@ -201,9 +201,10 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Tervetuloa AURiin! Luethan %sAURin käyttäjä ohjeen%s sekä %sTU-käyttäjän oppaan%s, kun tarvitset lisätietoa." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -217,8 +218,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "Muista äänestää suosikkipakettejasi!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Jotkin paketit saattavat olla tarjolla valmiina paketteina [community]-varastossa." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Jotkin paketit saattavat olla tarjolla valmiina paketteina [extra]-varastossa." #: html/home.php msgid "DISCLAIMER" @@ -267,8 +268,8 @@ msgstr "Poistopyyntö" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "Hallintapyyntö paketin poistamiseksi AUR:ista. Jos paketti on jollain tapaa rikki tai huono, mutta helposti korjattavissa, tulisi ensisijaisesti olla yhteydessä paketin ylläpitäjään ja viimekädessä pistää paketin hylkäämispyyntö menemään." +"the maintainer and file orphan request if necessary." +msgstr "" #: html/home.php msgid "Merge Request" @@ -310,10 +311,11 @@ msgstr "Keskustelu" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "Yleinen keskustelu AUR:iin ja Luotettuihin käyttäjiin liittyen käydään postitusluettelossa %saur-general%s. AUR-verkkokäyttöliittymän kehittämiseen liittyvä keskustelu käydään postitusluettelossa %saur-dev%s." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -324,9 +326,9 @@ msgstr "Virheiden raportointi" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "Jos löydät AUR-verkkokäyttöliittymästa virheen, täytä virheenilmoituslomake %svirheenseurannassamme%s. Käytä sivustoa %sainoastaan%s verkkokäyttöliittymän virheiden ilmoittamiseen. Ilmoittaaksesi pakettien virheistä, ota yhteys paketin ylläpitäjään tai jätä kommentti paketin sivulla." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "" #: html/home.php msgid "Package Search" @@ -526,8 +528,8 @@ msgid "Delete" msgstr "Poista" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Vain Luotetut käyttäjät, sekä kehittäjät voivat poistaa paketteja." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -567,8 +569,8 @@ msgid "Disown" msgstr "Hylkää" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "Vain Luotetut käyttäjät, sekä kehittäjät voivat hylätä paketteja." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -657,8 +659,8 @@ msgid "Merge" msgstr "Liitä" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Vain Luotetut käyttäjät, sekä kehittäjät voivat yhdistää paketteja." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -715,8 +717,8 @@ msgid "I accept the terms and conditions above." msgstr "Hyväksyn ylläolevat ehdot." #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Luotettu käyttäjä (TU)" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -727,8 +729,8 @@ msgid "Voting is closed for this proposal." msgstr "Tämän ehdoksen äänestys on päättynyt." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Vain Luotetut käyttäjät voivat äänestää." +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -1223,8 +1225,8 @@ msgstr "Kehittäjä" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "Luotettu käyttäjä & kehittäjä" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1326,10 +1328,6 @@ msgstr "" msgid "Normal user" msgstr "Tavallinen käyttäjä" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Luotettu käyttäjä (TU)" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Käyttäjätili hyllytetty" @@ -1402,6 +1400,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "Julkinen SSH avain" @@ -1825,22 +1832,22 @@ msgstr "Yhdistä pakettiin" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "Lähettämällä poistopyynnön pyydät Luotettua käyttäjää poistamaan pakettikannan. Tämän tyyppisiä pyyntöjä tulisi käyttää ainoastaan kaksoiskappaleisiin, laittomiin tai korjaamattoman rikkonaisiin paketteihin sekä ohjelmistoihin, jotka kehittäjä on hylännyt." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "Ennen yhdistämispyynnön lähettämistä, pyydä Luotettua käyttäjää poistamaan pakettikanta ja siirtämään sen äänet ja kommentit toiseen pakettikantaan. Paketin yhdistäminen ei vaikuta Git-varastoihin. Varmista, että päivität kohdepaketin Git-historian itse." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2097,8 +2104,8 @@ msgid "Registered Users" msgstr "Rekisteröityjä käyttäjiä" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Luotettuja käyttäjiä" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2283,7 +2290,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2337,3 +2344,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/fi_FI.po b/po/fi_FI.po index f3253433..e0193ec9 100644 --- a/po/fi_FI.po +++ b/po/fi_FI.po @@ -6,11 +6,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Finnish (Finland) (http://www.transifex.com/lfleischer/aurweb/language/fi_FI/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Finnish (Finland) (http://app.transifex.com/lfleischer/aurweb/language/fi_FI/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -131,15 +131,15 @@ msgid "Type" msgstr "" #: html/addvote.php -msgid "Addition of a TU" +msgid "Addition of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU" +msgid "Removal of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" msgstr "" #: html/addvote.php @@ -197,8 +197,9 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." msgstr "" #: html/home.php @@ -213,7 +214,7 @@ msgid "Remember to vote for your favourite packages!" msgstr "" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." +msgid "Some packages may be provided as binaries in [extra]." msgstr "" #: html/home.php @@ -263,7 +264,7 @@ msgstr "" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -306,9 +307,10 @@ msgstr "" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -320,8 +322,8 @@ msgstr "" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -522,7 +524,7 @@ msgid "Delete" msgstr "" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." +msgid "Only Package Maintainers and Developers can delete packages." msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php @@ -563,7 +565,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -653,7 +655,7 @@ msgid "Merge" msgstr "" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." +msgid "Only Package Maintainers and Developers can merge packages." msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php @@ -711,7 +713,7 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" +msgid "Package Maintainer" msgstr "" #: html/tu.php @@ -723,7 +725,7 @@ msgid "Voting is closed for this proposal." msgstr "" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -1219,7 +1221,7 @@ msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1322,10 +1324,6 @@ msgstr "" msgid "Normal user" msgstr "" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "" @@ -1398,6 +1396,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1821,22 +1828,22 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2093,7 +2100,7 @@ msgid "Registered Users" msgstr "" #: template/stats/general_stats_table.php -msgid "Trusted Users" +msgid "Package Maintainers" msgstr "" #: template/stats/updates_table.php @@ -2279,7 +2286,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2333,3 +2340,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/fr.po b/po/fr.po index 99d01460..b7942b3a 100644 --- a/po/fr.po +++ b/po/fr.po @@ -5,12 +5,12 @@ # Translators: # Alexandre Macabies , 2018 # Antoine Lubineau , 2012 -# Antoine Lubineau , 2012-2016 +# Antoine Lubineau , 2012-2016,2023 # Cedric Girard , 2011,2014,2016 # demostanis , 2020 # Kristien , 2020 # lordheavy , 2011 -# lordheavy , 2013-2014,2018 +# lordheavy , 2013-2014,2018,2022 # lordheavy , 2011-2012 # Lukas Fleischer , 2011 # Thibault , 2020 @@ -18,16 +18,16 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: French (http://www.transifex.com/lfleischer/aurweb/language/fr/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Antoine Lubineau , 2012-2016,2023\n" +"Language-Team: French (http://app.transifex.com/lfleischer/aurweb/language/fr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: fr\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #: html/404.php msgid "Page Not Found" @@ -143,16 +143,16 @@ msgid "Type" msgstr "Type" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Ajout d’un utilisateur de confiance." +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Suppression d’un utilisateur de confiance" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Suppression d’un utilisateur de confiance (inactivité non prévenue)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -209,9 +209,10 @@ msgstr "Rechercher les paquets que je co-maintiens" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Bienvenue sur AUR ! Veuillez lire les %sconsignes pour les utilisateurs d’AUR%s et les %sconsignes pour les utilisateurs de confiance%s pour plus d’information." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -225,8 +226,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "Pensez à voter pour vos paquets favoris !" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Certains paquets peuvent être disponibles sous forme binaire dans le dépôt [community]." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Certains paquets peuvent être disponibles sous forme binaire dans le dépôt [extra]." #: html/home.php msgid "DISCLAIMER" @@ -275,8 +276,8 @@ msgstr "Requête de suppression" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "Demande qu'un paquet soit supprimé d'AUR. Prière de ne pas l'utiliser si un paquet est cassé et que le problème peut être réglé facilement. À la place, contactez le mainteneur du paquet, et soumettez une requête de destitution si nécessaire." +"the maintainer and file orphan request if necessary." +msgstr "" #: html/home.php msgid "Merge Request" @@ -318,10 +319,11 @@ msgstr "Discussion" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "Les discussions générales en rapport avec AUR (Arch User Repository, dépôt des utilisateurs d’Arch Linux) et les TU (Trusted User, utilisateurs de confiance) ont lieu sur %saur-general%s. Pour les discussions en rapport avec le développement de l'interface web d'AUR, utilisez la mailing-list %saur-dev.%s" +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -332,9 +334,9 @@ msgstr "Rapports de bug" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "Si vous trouvez un bug dans l'interface web d'AUR, merci de remplir un rapport de bug sur le %sbug tracker%s. N’utilisez le tracker %sque%s pour les bugs de l'interface web d'AUR. Pour signaler un bug dans un paquet, contactez directement le mainteneur du paquet, ou laissez un commentaire sur la page du paquet." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "" #: html/home.php msgid "Package Search" @@ -486,7 +488,7 @@ msgstr "Les paquets sélectionnés n'ont pas été destitués, vérifiez la boî msgid "" "The selected packages have not been adopted, check the confirmation " "checkbox." -msgstr "" +msgstr "Les paquets sélectionnés n’ont pas été adoptés, vérifiez la case de confirmation." #: html/pkgbase.php lib/pkgreqfuncs.inc.php msgid "Cannot find package to merge votes and comments into." @@ -534,8 +536,8 @@ msgid "Delete" msgstr "Supprimer" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Seuls les Utilisateur de Confiance et les Développeurs peuvent effacer des paquets." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -575,8 +577,8 @@ msgid "Disown" msgstr "Destituer" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "Seuls les Utilisateur de Confiance et les Développeurs peuvent destituer des paquets." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -665,8 +667,8 @@ msgid "Merge" msgstr "Fusionner" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Seuls les Utilisateur de Confiance et les Développeurs peuvent fusionner des paquets." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -723,8 +725,8 @@ msgid "I accept the terms and conditions above." msgstr "J'accepte les modalités ci-avant." #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Utilisateur de confiance (TU)" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -735,8 +737,8 @@ msgid "Voting is closed for this proposal." msgstr "Le vote est clos pour cette proposition." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Seuls les Utilisateurs de Confiance sont autorisés à voter." +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -892,7 +894,7 @@ msgstr "Compte suspendu." #: aurweb/routers/accounts.py msgid "You do not have permission to suspend accounts." -msgstr "" +msgstr "Vous n’avez pas la permission de suspendre des comptes." #: lib/acctfuncs.inc.php #, php-format @@ -981,27 +983,27 @@ msgstr "Les détails du paquet ne peuvent pas être trouvés." #: aurweb/routers/auth.py msgid "Bad Referer header." -msgstr "" +msgstr "Mauvais en-tête Referer." #: aurweb/routers/packages.py msgid "You did not select any packages to be notified about." -msgstr "" +msgstr "Vous n’avez sélectionné aucun paquet pour être notifié." #: aurweb/routers/packages.py msgid "The selected packages' notifications have been enabled." -msgstr "" +msgstr "Les notifications des paquets sélectionnés ont été activées." #: aurweb/routers/packages.py msgid "You did not select any packages for notification removal." -msgstr "" +msgstr "Vous n’avez sélectionné aucun paquet pour la suppression de notification." #: aurweb/routers/packages.py msgid "A package you selected does not have notifications enabled." -msgstr "" +msgstr "Un paquet que vous avez sélectionné n’a pas les notifications activées." #: aurweb/routers/packages.py msgid "The selected packages' notifications have been removed." -msgstr "" +msgstr "Les notifications des paquets sélectionnés ont été supprimées." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can flag packages." @@ -1041,7 +1043,7 @@ msgstr "Vous n'avez sélectionné aucun paquet à supprimer." #: aurweb/routers/packages.py msgid "One of the packages you selected does not exist." -msgstr "" +msgstr "L’un des paquets que vous avez sélectionnés n’existe pas." #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been deleted." @@ -1053,15 +1055,15 @@ msgstr "Vous devez être authentifié avant de pouvoir adopter des paquets." #: aurweb/routers/package.py msgid "You are not allowed to adopt one of the packages you selected." -msgstr "" +msgstr "Vous n’êtes pas autorisé à adopter l’un des paquets que vous avez sélectionnés." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can disown packages." -msgstr "Vous devez être authentifié avant de pouvoir abandonner des paquets." +msgstr "Vous devez être authentifié avant de pouvoir destituer des paquets." #: aurweb/routers/packages.py msgid "You are not allowed to disown one of the packages you selected." -msgstr "" +msgstr "Vous n’êtes pas autorisé à destituer l’un des paquets que vous avez sélectionnés." #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to adopt." @@ -1069,7 +1071,7 @@ msgstr "Vous n'avez pas sélectionné de paquet à adopter." #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to disown." -msgstr "Vous n'avez sélectionné aucun paquet à abandonner." +msgstr "Vous n'avez sélectionné aucun paquet à destituer." #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been adopted." @@ -1077,7 +1079,7 @@ msgstr "Les paquets sélectionnés ont été adoptés." #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been disowned." -msgstr "Les paquets sélectionnés ont été abandonnés." +msgstr "Les paquets sélectionnés ont été destitués." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can vote for packages." @@ -1231,8 +1233,8 @@ msgstr "Développeur" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "Utilisateur de confiance (TU) et Développeur" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1334,10 +1336,6 @@ msgstr "Votre nom d'utilisateur est le nom que vous utilisez pour vous connecter msgid "Normal user" msgstr "Utilisateur normal" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Utilisateur de confiance (TU)" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Compte suspendu" @@ -1410,6 +1408,15 @@ msgid "" " the Arch User Repository." msgstr "L'information suivante est requise uniquement si vous voulez soumettre des paquets sur AUR" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "Clé SSH publique" @@ -1590,6 +1597,7 @@ msgid "%d pending request" msgid_plural "%d pending requests" msgstr[0] "%d requête en attente" msgstr[1] "%d requêtes en attente" +msgstr[2] "%d requêtes en attente" #: template/pkgbase_actions.php msgid "Adopt Package" @@ -1833,26 +1841,26 @@ msgstr "Fusionner dans" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "En soumettant une requète de suppression, vous demandez à un utilisateur de confiance de supprimer le paquet de base. Ce type de requète doit être utilisé pour les doublons, les logiciels abandonnés par l'upstream ainsi que pour les paquets illégaux ou irréparables." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "En soumettant une requète de fusion, vous demandez à un utilisateur de confiance de supprimer le paquet de base et de transférer les votes et les commentaires vers un autre paquet de base. Fusionner un paquet n'impacte pas le dépot Git correspondant. Assurez-vous de mettre à jour l'historique Git du paquet cible vous-même." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "En soumettant une requète pour rendre orphelin, vous demandez à un utilisateur de confiance de retirer le mainteneur du paquet de base. Merci de ne faire ceci que si le paquet nécessite l'action d'un mainteneur, que le mainteneur ne répond pas et que vous avez préalablement essayé de contacter le mainteneur." +msgstr "" #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -1864,6 +1872,7 @@ msgid "%d package request found." msgid_plural "%d package requests found." msgstr[0] "%d paquet demandé trouvé." msgstr[1] "%d paquets demandés trouvés." +msgstr[2] "%d paquets demandés trouvés." #: template/pkgreq_results.php template/pkg_search_results.php #, php-format @@ -1888,6 +1897,7 @@ msgid "~%d day left" msgid_plural "~%d days left" msgstr[0] "~%d jour restant" msgstr[1] "~%d jours restants" +msgstr[2] "~%d jours restants" #: template/pkgreq_results.php #, php-format @@ -1895,6 +1905,7 @@ msgid "~%d hour left" msgid_plural "~%d hours left" msgstr[0] "~%d heure restante" msgstr[1] "%d heures restantes" +msgstr[2] "%d heures restantes" #: template/pkgreq_results.php msgid "<1 hour left" @@ -2023,6 +2034,7 @@ msgid "%d package found." msgid_plural "%d packages found." msgstr[0] "%d paquet trouvé." msgstr[1] "%d paquets trouvés." +msgstr[2] "%d paquets trouvés." #: template/pkg_search_results.php msgid "Version" @@ -2058,7 +2070,7 @@ msgstr "Adopter des paquets" #: template/pkg_search_results.php msgid "Disown Packages" -msgstr "Abandonner les paquets" +msgstr "Destituer les paquets" #: template/pkg_search_results.php msgid "Delete Packages" @@ -2105,8 +2117,8 @@ msgid "Registered Users" msgstr "Utilisateurs enregistrés" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Utilisateurs de confiance (TU)" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2291,8 +2303,8 @@ msgstr "{user} [1] a supprimé {pkgbase} [2].\n\nVous ne recevrez plus de notifi #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" -msgstr "Rappel du vote du TU : proposition {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" +msgstr "" #: scripts/notify.py #, python-brace-format @@ -2303,15 +2315,15 @@ msgstr "N'oubliez pas de voter sur la proposition {id} [1]. La période de vote #: aurweb/routers/accounts.py msgid "Invalid account type provided." -msgstr "" +msgstr "Type de compte choisi invalide." #: aurweb/routers/accounts.py msgid "You do not have permission to change account types." -msgstr "" +msgstr "Vous n’avez pas la permission de changer le type de compte." #: aurweb/routers/accounts.py msgid "You do not have permission to change this user's account type to %s." -msgstr "" +msgstr "Vous n’avez pas la permission de changer le type de compte de cet utilisateur en %s." #: aurweb/packages/requests.py msgid "No due existing orphan requests to accept for %s." @@ -2319,11 +2331,11 @@ msgstr "" #: aurweb/asgi.py msgid "Internal Server Error" -msgstr "" +msgstr "Erreur interne du serveur" #: templates/errors/500.html msgid "A fatal error has occurred." -msgstr "" +msgstr "Une erreur fatale s’est produite." #: templates/errors/500.html msgid "" @@ -2333,7 +2345,7 @@ msgstr "" #: aurweb/scripts/notify.py msgid "AUR Server Error" -msgstr "" +msgstr "Erreur du serveur AUR" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html @@ -2345,3 +2357,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/he.po b/po/he.po index cd4a0f87..e39a5940 100644 --- a/po/he.po +++ b/po/he.po @@ -3,17 +3,17 @@ # This file is distributed under the same license as the AURWEB package. # # Translators: -# GenghisKhan , 2016 +# gk , 2016 # Lukas Fleischer , 2011 -# Yaron Shahrabani , 2016-2022 +# Yaron Shahrabani , 2016-2023 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Hebrew (http://www.transifex.com/lfleischer/aurweb/language/he/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Yaron Shahrabani , 2016-2023\n" +"Language-Team: Hebrew (http://app.transifex.com/lfleischer/aurweb/language/he/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -134,16 +134,16 @@ msgid "Type" msgstr "סוג" #: html/addvote.php -msgid "Addition of a TU" -msgstr "הוספת משתמש מהימן" +msgid "Addition of a Package Maintainer" +msgstr "הוספת מתחזקים לחבילה" #: html/addvote.php -msgid "Removal of a TU" -msgstr "הסרת משתמש מהימן" +msgid "Removal of a Package Maintainer" +msgstr "הסרת מתחזקים מהחבילה" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "הסרת משתמש מהימן (חוסר פעילות בלתי מוצהרת)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "הסרת מתחזקי חבילה (חוסר פעילות לא מוצהרת)" #: html/addvote.php msgid "Amendment of Bylaws" @@ -200,9 +200,10 @@ msgstr "חיפוש אחר חבילה שאני מתחזק המשנה שלה" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "ברוך בואך ל־AUR, מאגר תרומות המשתמשים של ארץ׳! נא לקרוא את %sהכללים למשתמש ב־AUR%s ואת %sהכללים למשתמשים מהימנים ב־AUR%s." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "ברוך בואך ל־AUR! נא לקרוא את %sההנחיות למשתמשים ב־AUR%s למידע נוסף ואת %sהנחיות ההגשה ל־AUR%s אם מעניין אותך לתרום PKGBUILD." #: html/home.php #, php-format @@ -216,8 +217,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "לא לשכוח להצביע לחבילות המועדפות עליך!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "יתכן שחלק מהחבילות מסופקות בתור קבצים בינריים תחת [community] (קהילה)." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "יתכן שחלק מהחבילות מסופקות בתור קבצים בינריים תחת [extra] (קהילה)." #: html/home.php msgid "DISCLAIMER" @@ -266,8 +267,8 @@ msgstr "בקשת מחיקה" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "ניתן לבקש הסרת חבילה ממאגר המשתמשים של ארץ׳. אין להשתמש בזה אם יש תקלה בחבילה וניתן לתקן אותה בקלות. במקום זאת, יש ליצור קשר עם מתחזק החבילה ולהגיש בקשת יתומה אם יש צורך." +"the maintainer and file orphan request if necessary." +msgstr "אפשר לבקש הסרת חבילה ממאגר המשתמשים של Arch ‏(AUR). נא לא להשתמש בזה אם החבילה פגומה ואפשר לתקן אותה בקלות. במקום, יש ליצור קשר עם מתחזקי החבילה ולהגיש בקשת יתמות במקרה הצורך." #: html/home.php msgid "Merge Request" @@ -309,10 +310,11 @@ msgstr "דיון" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "הדיון הכללי על מאגר המשתמשים של ארץ׳ (AUR) ומבנה המשתמשים המהימנים מתנהל ברשימה %saur-general%s. לדיון בנוגע לפיתוח של המנשק של AUR, יש להשתמש ברשימה %saur-dev%s." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "דיון כללי בנוגע למאגר המשתמשים של Arch‏ (AUR) ומבנה מתחזקי החבילות מתרחש ב־%saur-general%s. לדיון בנוגע לפיתוח האתר של AUR, יש להשתמש ברשימת הדיוור %saur-dev%s." #: html/home.php msgid "Bug Reporting" @@ -323,9 +325,9 @@ msgstr "דיווח על באגים" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "אם נתקלת בתקלה במנשק הדפדפן של AUR, נא להגיש דיווח על תקלה ב%sמערכת ניהול התקלות%s שלנו. יש להשתמש במערכת ניהול התקלות כדי לדווח על תקלות במנשק הדפדפן %sבלבד%s. כדי לדווח על תקלות עם אריזה יש ליצור קשר עם מתחזק החבילה או להשאיר הערה בעמוד החבילה בהתאם." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "אם מצאת תקלה באתר של AUR, נא למלא דוח תקלה ב%sעוקב התקלות%s שלנו. אפשר להשתמש בעוקב כדי לדווח על תקלות באתר של AUR %sבלבד%s. כדי לדווח על תקלות עם אריזות יש ליצור קשר עם המתחזקים או להשאיר תגובה בעמוד החבילה המתאים." #: html/home.php msgid "Package Search" @@ -525,8 +527,8 @@ msgid "Delete" msgstr "מחיקה" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "רק משתמשים מהימנים ומפתחים יכולים למחוק חבילות." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "רק מתחזקי ומפתחי חבילות יכולים למחוק חבילות." #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -566,8 +568,8 @@ msgid "Disown" msgstr "ניתוק בעלות" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "רק משתמשים מהימנים ומפתחים יכולים לנתק בעלות של חבילות." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "רק מתחזקי ומפתחי חבילות יכולים לנשל חבילות." #: html/pkgflagcomment.php msgid "Flag Comment" @@ -656,8 +658,8 @@ msgid "Merge" msgstr "מיזוג" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "רק משתמשים מהימנים ומפתחים יכולים למזג חבילות." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "רק מתחזקי ומפתחי חבילות יכולים למזג חבילות." #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -714,8 +716,8 @@ msgid "I accept the terms and conditions above." msgstr "התנאים שלעיל מקובלים עלי." #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "משתמש מהימן" +msgid "Package Maintainer" +msgstr "מתחזקי חבילה" #: html/tu.php msgid "Could not retrieve proposal details." @@ -726,8 +728,8 @@ msgid "Voting is closed for this proposal." msgstr "ההצבעה סגורה עבור הצעה זו." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "רק משתמשים מהימנים מורשים להצביע." +msgid "Only Package Maintainers are allowed to vote." +msgstr "רק למתחזקי חבילה מותר להצביע." #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -1222,8 +1224,8 @@ msgstr "מפתח" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "משתמש מהימן ומפתח" +msgid "Package Maintainer & Developer" +msgstr "מתחזקי ומפתחי חבילה" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1325,10 +1327,6 @@ msgstr "שם המשתמש שלך הוא השם שישמש אותך לכניסה msgid "Normal user" msgstr "משתמש רגיל" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "משתמשים אמינים" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "חשבון מושעה" @@ -1401,6 +1399,15 @@ msgid "" " the Arch User Repository." msgstr "המידע הבא נחוץ רק אם ברצונך להגיש חבילות למאגר החבילות של ארץ׳." +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "אפשר לציין מגוון מפתחות SSH, אחד בשורה, אין משמעות לשורה ריקה." + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "הסתרת הערות שנמחקו" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "מפתח SSH ציבורי" @@ -1826,26 +1833,26 @@ msgstr "מיזוג לתוך" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "בהגשת בקשה למחיקה, משתמש מהימן ישקול אם למחוק בסיס חבילה. סוג כזה של בקשה יכול לשמש במקרים של כפילויות, תכנית שנזנחה במקור לצד חבילה בלתי חוקית או שבורה באופן שלא ניתן לשקם." +msgstr "בקשת מחיקה היא דרישה ממתחזקי החבילה למחוק את בסיס החבילה. יש להשתמש בסוג הזה של הבקשה על כפילויות, תוכנה שננטשה במקור, וגם חבילות בלתי חוקיות ופגומות באופן שלא ניתן לתיקון." #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "הגשת בקשת מיזוג מופנית למשתמש מהימן לטובת מחיקת בסיס חבילה והעברת ההצבעות וההערות שלו לבסיס חבילה אחר. מיזוג חבילה לא משפיע על מאגרי ה־Git הקשורים אליו. יש לוודא שעדכנת את היסטוריית ה־Git של חבילת היעד בעצמך." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "בקשת מיזוג היא דרישה ממתחזקי החבילה למחוק את בסיס החבילה ולהעביר את ההצבעות וההערות לבסיס חבילה אחר. מיזוג חבילה לא משפיע על מאגרי ה־Git התואמים. עדכון היסטוריית ה־Git של חבילת היעד הוא באחריותך. " #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "הגשת בקשת יתומה מופנית למשתמש מהימן לטובת ביטול שייכות בסיס חבילה. נא להגיש בקשה זו רק אם החבילה דורשת פעולת תחזוקה, המתחזק אינו זמין וכבר ניסית ליצור קשר עם המתחזק בעבר." +msgstr "בקשת יתמות היא דרישה ממתחזקי החבילה לנשל את בסיס החבילה. נא עשות את זה רק אם החבילה צריכה פעולת תחזוקה, המתחזקים לא זמינים למשך תקופה וכבר ניסית ליצור קשר עם המתחזקים בעבר." #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -2106,8 +2113,8 @@ msgid "Registered Users" msgstr "משתמשים רשומים" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "משתמשים מהימנים" +msgid "Package Maintainers" +msgstr "מתחזקי החבילה" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2292,8 +2299,8 @@ msgstr " {pkgbase} [2] נמחקה על ידי{user} [1].\n\nלא תישלחנה #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" -msgstr "תזכורת הצבעה למשתמש מהימן: הצעה {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" +msgstr "תזכורת הצבעה לתחזוקת חבילה: הצעה {id}" #: scripts/notify.py #, python-brace-format @@ -2339,10 +2346,42 @@ msgstr "שגיאת שרת ה־AUR" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "הערות הסגירה התואמות של בקשת החבילה…" #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." -msgstr "" +msgstr "פעולה זו תסגור בקשות חבילות ממתינות שקשורות אליה. אם %sתגובות%s מושמטות, תיווצר תגובת סגירה אוטומטית." + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "מוקצית" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "להציג עוד %d" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "תלויות" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "החשבון לא נמחק, נא לבדוק את תיבת האישור." + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "ביטול" + +#: templates/requests.html +msgid "Package name" +msgstr "שם החבילה" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "ראוי לשים לב שהסתרת כתובת הדוא״ל שלך תעביר אותה לרשימת העותק המוסתר בהתראות לבקשה. אם מישהו יגיב להתראות האלה, ההודעות לא תגענה אליך. עם זאת, תגובות בדרך כלל נשלחות לרשימת הדיוור והן אמורות להופיע בארכיון." diff --git a/po/hi_IN.po b/po/hi_IN.po index 37fd082e..f8a66539 100644 --- a/po/hi_IN.po +++ b/po/hi_IN.po @@ -3,15 +3,15 @@ # This file is distributed under the same license as the AURWEB package. # # Translators: -# Panwar108 , 2018,2020-2021 +# Panwar108 , 2018,2020-2022 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Hindi (India) (http://www.transifex.com/lfleischer/aurweb/language/hi_IN/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Panwar108 , 2018,2020-2022\n" +"Language-Team: Hindi (India) (http://app.transifex.com/lfleischer/aurweb/language/hi_IN/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -132,16 +132,16 @@ msgid "Type" msgstr "प्रकार" #: html/addvote.php -msgid "Addition of a TU" -msgstr "विश्वसनीय उपयोक्ता जोड़ना" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "विश्वसनीय उपयोक्ता हटाना" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "विश्वसनीय उपयोक्ता हटाना (अघोषित निष्क्रियता)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -198,9 +198,10 @@ msgstr "मेरे द्वारा सह-अनुरक्षित प #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "AUR में स्वागत है! अधिक जानकारी हेतु %sAUR उपयोक्ता%s व %sAUR विश्वसनीय उपयोक्ता%s दिशा-निर्देश पढ़ें।" +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -214,8 +215,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "अपने पसंदीदा पैकेज हेतु मतदान अवश्य करें!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "[community] के कुछ पैकेज बाइनरी फाइल के रूप में उपलब्ध हो सकते हैं।" +msgid "Some packages may be provided as binaries in [extra]." +msgstr "[extra] के कुछ पैकेज बाइनरी फाइल के रूप में उपलब्ध हो सकते हैं।" #: html/home.php msgid "DISCLAIMER" @@ -264,8 +265,8 @@ msgstr "पैकेज हटाने हेतु अनुरोध" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "आर्च उपयोक्ता पैकेज-संग्रह से पैकेज हटाने हेतु अनुरोध। कृपया पैकेज उपयोग संबंधी समस्या होने पर इसका उपयोग न करें। उचित होगा कि पैकेज अनुरक्षक से संपर्क करें व आवश्यकता हो तो निरर्थक पैकेज हेतु अनुरोध करें।" +"the maintainer and file orphan request if necessary." +msgstr "" #: html/home.php msgid "Merge Request" @@ -307,10 +308,11 @@ msgstr "चर्चा" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "आर्च उपयोक्ता पैकेज-संग्रह (AUR) व विश्वसनीय उपयोक्ता संरचना संबंधी सामान्य चर्चा %saur-general%s पर होती है। AUR वेब अंतरफलक के विकास संबंधी चर्चा हेतु %saur-dev%s ईमेल-सूची उपयोग करें।" +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -321,9 +323,9 @@ msgstr "समस्या हेतु रिपोर्ट" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "AUR वेब अंतरफलक में समस्या होने पर कृपया हमारे %sसमस्या ट्रैकर%s पर समस्या रिपोर्ट दर्ज करें। ट्रैकर का उपयोग %sकेवल%s AUR वेब अंतरफलक संबंधी समस्याओं के लिए ही करें। पैकेज समस्याओं हेतु पैकेज अनुरक्षक से संपर्क करें या उपयुक्त पैकेज के पृष्ठ पर टिप्पणी करें।" +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "" #: html/home.php msgid "Package Search" @@ -523,8 +525,8 @@ msgid "Delete" msgstr "हटाएँ" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "केवल विश्वसनीय उपयोक्ता व व सॉफ्टवेयर विकासकर्ता ही पैकेज हटा सकते हैं।" +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -564,8 +566,8 @@ msgid "Disown" msgstr "स्वामित्व निरस्त करें" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "केवल विश्वसनीय उपयोक्ता व व सॉफ्टवेयर विकासकर्ता ही पैकेज स्वामित्व निरस्त कर सकते हैं।" +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -654,8 +656,8 @@ msgid "Merge" msgstr "विलय करें" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "केवल विश्वसनीय उपयोक्ता व व सॉफ्टवेयर विकासकर्ता ही पैकेज विलय कर सकते हैं।" +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -712,8 +714,8 @@ msgid "I accept the terms and conditions above." msgstr "मैं ऊपर दिए गए नियम व शर्तों को स्वीकारता हूँ।" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "विश्वसनीय उपयोक्ता" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -724,8 +726,8 @@ msgid "Voting is closed for this proposal." msgstr "इस प्रस्ताव हेतु वोट प्रक्रिया बंद है।" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "केवल विश्वसनीय उपयोक्ता ही मतदान कर सकते हैं।" +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -897,7 +899,7 @@ msgstr "अनुचित उपयोक्ता नाम या कूट #: lib/acctfuncs.inc.php msgid "An error occurred trying to generate a user session." -msgstr "उपयोक्ता सत्र बनाने हेतु त्रुटि।" +msgstr "उपयोक्ता सत्र बनाने समय त्रुटि हुई।" #: lib/acctfuncs.inc.php msgid "Invalid e-mail and reset key combination." @@ -1220,8 +1222,8 @@ msgstr "सॉफ्टवेयर विकासकर्ता" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "विश्वसनीय उपयोक्ता व सॉफ्टवेयर विकासकर्ता" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1323,10 +1325,6 @@ msgstr "लॉगिन हेतु प्रयुक्त नाम ही msgid "Normal user" msgstr "सामान्य उपयोक्ता" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "विश्वसनीय उपयोक्ता" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "निलंबित अकाउंट" @@ -1399,6 +1397,15 @@ msgid "" " the Arch User Repository." msgstr "निम्नलिखित जानकारी केवल आर्च उपयोक्ता पैकेज-संग्रह में पैकेज निवेदन करने हेतु ही आवश्यक है।" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "सार्वजनिक एसएसएच कुंजी" @@ -1822,26 +1829,26 @@ msgstr "इसमें विलय करें" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "हटाने हेतु अनुरोध से अभिप्राय है विश्वसनीय उपयोक्ता को पैकेज बेस हटाने हेतु निवेदन। यह प्रतिरूपित प्रोग्राम, स्रोत द्वारा त्यागे गए सॉफ्टवेयर के साथ ही अवैध व समाधान विहीन समस्यात्मक पैकेज हेतु उचित होता है।" +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "विलय अनुरोध से अभिप्राय है विश्वसनीय उपयोक्ता को पैकेज बेस हटाकर मत व टिप्पणियाँ अन्य पैकेज बेस में अंतरण हेतु निवेदन। पैकेज बेस हटाने से संबंधित पैकेज-संग्रह प्रभावित नहीं होते हैं। सुनिश्चित करें कि आप लक्षित पैकेज का Git वृतांत स्वयं अपडेट करें।" +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "निरर्थक पैकेज अनुरोध से अभिप्राय है विश्वसनीय उपयोक्ता को पैकेज स्वामित्व निरस्त करने हेतु निवेदन। अनुरक्षक कार्य अनिवार्यता, अनुरक्षक की अघोषित अनुपस्थिति और स्वयं अनुरक्षक से संपर्क हेतु प्रयास होने पर ही कृपया ऐसा करें।" +msgstr "" #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -2094,8 +2101,8 @@ msgid "Registered Users" msgstr "पंजीकृत उपयोक्ता" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "विश्वसनीय उपयोक्ता" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2280,8 +2287,8 @@ msgstr "{user} [1] द्वारा {pkgbase} [2] हटाया गया। #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" -msgstr "विश्वसनीय उपयोक्ता मतदान सूचक : प्रस्ताव {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" +msgstr "" #: scripts/notify.py #, python-brace-format @@ -2308,29 +2315,61 @@ msgstr "%s स्वीकारनें हेतु कोई निरर् #: aurweb/asgi.py msgid "Internal Server Error" -msgstr "" +msgstr "आंतरिक सर्वर त्रुटि" #: templates/errors/500.html msgid "A fatal error has occurred." -msgstr "" +msgstr "गंभीर त्रुटि हुई।" #: templates/errors/500.html msgid "" "Details have been logged and will be reviewed by the postmaster posthaste. " "We apologize for any inconvenience this may have caused." -msgstr "" +msgstr "संबंधित सूचना लॉग फाइल में दर्ज की जा चुकी है एवं अतिशीघ्र ही पोस्ट प्रबंधक द्वारा उसकी समीक्षा की जाएगी। इस कारण हुई किसी भी प्रकार की असुविधा हेतु खेद है।" #: aurweb/scripts/notify.py msgid "AUR Server Error" -msgstr "" +msgstr "AUR सर्वर त्रुटि" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "पैकेज अनुरोध समापन संबंधी टिप्पणियाँ..." #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." +msgstr "इस कार्य द्वारा संबंधित सभी लंबित पैकेज अनुरोध बंद हो जाएँगे। %sटिप्पणियाँ%s न होने की स्थिति में एक समापन टिप्पणी का स्वतः ही सृजन होगा।" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." msgstr "" diff --git a/po/hr.po b/po/hr.po index 4932bd7e..931d6fa5 100644 --- a/po/hr.po +++ b/po/hr.po @@ -7,11 +7,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Croatian (http://www.transifex.com/lfleischer/aurweb/language/hr/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Lukas Fleischer , 2011\n" +"Language-Team: Croatian (http://app.transifex.com/lfleischer/aurweb/language/hr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -132,15 +132,15 @@ msgid "Type" msgstr "Tip" #: html/addvote.php -msgid "Addition of a TU" +msgid "Addition of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU" +msgid "Removal of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" msgstr "" #: html/addvote.php @@ -198,8 +198,9 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." msgstr "" #: html/home.php @@ -214,7 +215,7 @@ msgid "Remember to vote for your favourite packages!" msgstr "" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." +msgid "Some packages may be provided as binaries in [extra]." msgstr "" #: html/home.php @@ -264,7 +265,7 @@ msgstr "" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -307,9 +308,10 @@ msgstr "Rasprava" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -321,8 +323,8 @@ msgstr "" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -523,7 +525,7 @@ msgid "Delete" msgstr "" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." +msgid "Only Package Maintainers and Developers can delete packages." msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php @@ -564,7 +566,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -654,7 +656,7 @@ msgid "Merge" msgstr "" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." +msgid "Only Package Maintainers and Developers can merge packages." msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php @@ -712,8 +714,8 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Pouzdan korisnik" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -724,7 +726,7 @@ msgid "Voting is closed for this proposal." msgstr "Glasanje je zaključeno za ovaj prijedlog" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -1220,7 +1222,7 @@ msgstr "Developer" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1323,10 +1325,6 @@ msgstr "" msgid "Normal user" msgstr "Običan korisnik" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Pouzdan korisnik" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Račun je suspendiran" @@ -1399,6 +1397,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1823,22 +1830,22 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2099,7 +2106,7 @@ msgid "Registered Users" msgstr "" #: template/stats/general_stats_table.php -msgid "Trusted Users" +msgid "Package Maintainers" msgstr "" #: template/stats/updates_table.php @@ -2285,7 +2292,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2339,3 +2346,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/hu.po b/po/hu.po index 51894457..8745b58f 100644 --- a/po/hu.po +++ b/po/hu.po @@ -11,11 +11,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Hungarian (http://www.transifex.com/lfleischer/aurweb/language/hu/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: PB, 2020\n" +"Language-Team: Hungarian (http://app.transifex.com/lfleischer/aurweb/language/hu/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -136,16 +136,16 @@ msgid "Type" msgstr "Típus" #: html/addvote.php -msgid "Addition of a TU" -msgstr "TU hozzáadása" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "TU eltávolítása" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "TU eltávolítása (be nem jelentett inaktivitás)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -202,9 +202,10 @@ msgstr "Csomagok keresése, amelyeknek társkarbantartója vagyok" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Üdvözlünk az AUR-ban! További információért olvasd el az %sAUR felhasználói irányelveket%s és az %sAUR TU irányelveket%s." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -218,8 +219,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "Ne felejts el szavazni kedvenc csomagjaidra!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Néhány csomagot lehet, hogy a [community] binárisként szolgáltat." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Néhány csomagot lehet, hogy a [extra] binárisként szolgáltat." #: html/home.php msgid "DISCLAIMER" @@ -268,8 +269,8 @@ msgstr "Törlési kérelem" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "Egy csomag Arch User Repositoriból való törlésének kérése. Kérünk, ne használd ezt, ha a csomag törött, és könnyen javítható. Ehelyett vedd fel a kapcsolatot a csomag karbantartójával, és tölts ki megtagadási kérelmet, ha szükséges." +"the maintainer and file orphan request if necessary." +msgstr "" #: html/home.php msgid "Merge Request" @@ -311,10 +312,11 @@ msgstr "Megbeszélés" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "Az Arch User Repositoryval (AUR) és a Trusted User struktúrával kapcsolatos általános tanácskozás helye az %saur-general%s. Az AUR webes felületének fejlesztésével kapcsolatos tanácskozáshoz az %saur-dev%s levelezőlista használandó." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -325,9 +327,9 @@ msgstr "Hibajelentés" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "Ha találsz egy hibát az AUR webes felületén, kérünk, tölts ki egy hibajelentést a %shibakövetőnkben%s. A hibakövetőt %scsak%s az AUR webes felületén található hibák jelentésére használd. Csomagolási hibák jelentéséhez lépj kapcsolatba a csomag fenntartójával, vagy hagyj egy hozzászólást a megfelelő csomag oldalán." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "" #: html/home.php msgid "Package Search" @@ -527,8 +529,8 @@ msgid "Delete" msgstr "Törlés" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Csak megbízható felhasználók és fejlesztők tudnak csomagokat törölni." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -568,8 +570,8 @@ msgid "Disown" msgstr "Megtagadás" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "Csak megbízható felhasználók és fejlesztők tudnak megtagadni csomagokat." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -658,8 +660,8 @@ msgid "Merge" msgstr "Beolvasztás" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Csak megbízható felhasználók és fejlesztők tudnak csomagokat beolvasztani." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -716,8 +718,8 @@ msgid "I accept the terms and conditions above." msgstr "Elfogadom a feljebb megadott feltételeket." #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Megbízható felhasználó" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -728,8 +730,8 @@ msgid "Voting is closed for this proposal." msgstr "A szavazás lezárult erre az indítványra." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "A szavazás csak megbízható felhasználóknak engedélyezett." +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -1224,8 +1226,8 @@ msgstr "Fejlesztő" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "Megbízható felhasználó és fejlesztő" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1327,10 +1329,6 @@ msgstr "A felhasználóneved a bejelentkezéshez használt név lesz. Látható msgid "Normal user" msgstr "Normál felhasználó" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Megbízható felhasználó" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Felhasználói fiók felfüggesztve" @@ -1403,6 +1401,15 @@ msgid "" " the Arch User Repository." msgstr "Az alábbi információ csak akkor szükséges, ha csomagokat szeretnél beküldeni az Arch User Repositoryba." +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "Nyilvános SSH kulcs" @@ -1826,26 +1833,26 @@ msgstr "Beolvasztás ebbe:" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "Törlési kérelem beküldésével megkérsz egy megbízható felhasználót, hogy törölje az alapcsomagot. Ez a típusú kérelem duplikátumok, főági fejlesztők által felhagyott szoftverek, valamint illegális és helyrehozhatatlanul elromlott csomagok esetén használható." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "Beolvasztási kérelem beküldésével megkérsz egy megbízható felhasználót, hogy törölje az alapcsomagot, és vigye át a szavazatait és hozzászólásait egy másik alapcsomaghoz. Egy csomag egyesítése nem érinti a kapcsolódó Git tárolókat. Győződj meg róla, hogy frissítetted magadnak a célcsomag Git történetét." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "Megtagadási kérelem beküldésével megkérsz egy megbízható felhasználót, hogy tegye árvává az alapcsomagot. Kérünk, hogy ezt csak akkor tedd, ha a csomag igényel fenntartói műveletet, a fenntartó eltűnt, és előzőleg már megpróbáltad felvenni a kapcsolatot a fenntartóval." +msgstr "" #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -2098,8 +2105,8 @@ msgid "Registered Users" msgstr "Regisztrált felhasználók" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Megbízható felhasználók" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2284,8 +2291,8 @@ msgstr "{user} [1] törölte a(z) {pkgbase} [2] csomagt.\n\nTöbbé nem fogsz é #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" -msgstr "Szavazás megbízható felhasználóról: javaslat {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" +msgstr "" #: scripts/notify.py #, python-brace-format @@ -2338,3 +2345,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/id.po b/po/id.po index 75a6c98b..62650942 100644 --- a/po/id.po +++ b/po/id.po @@ -8,11 +8,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Indonesian (http://www.transifex.com/lfleischer/aurweb/language/id/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: se7entime , 2016\n" +"Language-Team: Indonesian (http://app.transifex.com/lfleischer/aurweb/language/id/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -133,16 +133,16 @@ msgid "Type" msgstr "Tipe" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Tambahan dari TU" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Penghapusan dari TU" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Penghapusan dari TU (tanpa aktifitas yang tidak dideklarasikan)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -199,9 +199,10 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Selamat datang di AUR! Harap baca %sPedoman Pengguna AUR%s dan %sPedoman TU AUR%s untuk info lebih lanjut." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -215,7 +216,7 @@ msgid "Remember to vote for your favourite packages!" msgstr "Ingat untuk memberika suara untuk paket favorit anda!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." +msgid "Some packages may be provided as binaries in [extra]." msgstr "Beberapa paket mungkin disediakan sebagai binabinari di [comunity]." #: html/home.php @@ -265,7 +266,7 @@ msgstr "" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -308,9 +309,10 @@ msgstr "" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -322,8 +324,8 @@ msgstr "" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -524,7 +526,7 @@ msgid "Delete" msgstr "" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." +msgid "Only Package Maintainers and Developers can delete packages." msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php @@ -565,7 +567,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -655,7 +657,7 @@ msgid "Merge" msgstr "" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." +msgid "Only Package Maintainers and Developers can merge packages." msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php @@ -713,7 +715,7 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" +msgid "Package Maintainer" msgstr "" #: html/tu.php @@ -725,7 +727,7 @@ msgid "Voting is closed for this proposal." msgstr "" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -1221,7 +1223,7 @@ msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1324,10 +1326,6 @@ msgstr "" msgid "Normal user" msgstr "" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "" @@ -1400,6 +1398,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1822,22 +1829,22 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2090,7 +2097,7 @@ msgid "Registered Users" msgstr "" #: template/stats/general_stats_table.php -msgid "Trusted Users" +msgid "Package Maintainers" msgstr "" #: template/stats/updates_table.php @@ -2276,7 +2283,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2330,3 +2337,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/id_ID.po b/po/id_ID.po index d01294c8..4840ca1e 100644 --- a/po/id_ID.po +++ b/po/id_ID.po @@ -6,11 +6,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Indonesian (Indonesia) (http://www.transifex.com/lfleischer/aurweb/language/id_ID/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Indonesian (Indonesia) (http://app.transifex.com/lfleischer/aurweb/language/id_ID/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -131,15 +131,15 @@ msgid "Type" msgstr "" #: html/addvote.php -msgid "Addition of a TU" +msgid "Addition of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU" +msgid "Removal of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" msgstr "" #: html/addvote.php @@ -197,8 +197,9 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." msgstr "" #: html/home.php @@ -213,7 +214,7 @@ msgid "Remember to vote for your favourite packages!" msgstr "" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." +msgid "Some packages may be provided as binaries in [extra]." msgstr "" #: html/home.php @@ -263,7 +264,7 @@ msgstr "" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -306,9 +307,10 @@ msgstr "" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -320,8 +322,8 @@ msgstr "" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -522,7 +524,7 @@ msgid "Delete" msgstr "" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." +msgid "Only Package Maintainers and Developers can delete packages." msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php @@ -563,7 +565,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -653,7 +655,7 @@ msgid "Merge" msgstr "" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." +msgid "Only Package Maintainers and Developers can merge packages." msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php @@ -711,7 +713,7 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" +msgid "Package Maintainer" msgstr "" #: html/tu.php @@ -723,7 +725,7 @@ msgid "Voting is closed for this proposal." msgstr "" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -1219,7 +1221,7 @@ msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1322,10 +1324,6 @@ msgstr "" msgid "Normal user" msgstr "" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "" @@ -1398,6 +1396,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1820,22 +1827,22 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2088,7 +2095,7 @@ msgid "Registered Users" msgstr "" #: template/stats/general_stats_table.php -msgid "Trusted Users" +msgid "Package Maintainers" msgstr "" #: template/stats/updates_table.php @@ -2274,7 +2281,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2328,3 +2335,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/is.po b/po/is.po index a7a88b04..0ec59559 100644 --- a/po/is.po +++ b/po/is.po @@ -6,11 +6,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Icelandic (http://www.transifex.com/lfleischer/aurweb/language/is/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Icelandic (http://app.transifex.com/lfleischer/aurweb/language/is/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -131,15 +131,15 @@ msgid "Type" msgstr "" #: html/addvote.php -msgid "Addition of a TU" +msgid "Addition of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU" +msgid "Removal of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" msgstr "" #: html/addvote.php @@ -197,8 +197,9 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." msgstr "" #: html/home.php @@ -213,7 +214,7 @@ msgid "Remember to vote for your favourite packages!" msgstr "" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." +msgid "Some packages may be provided as binaries in [extra]." msgstr "" #: html/home.php @@ -263,7 +264,7 @@ msgstr "" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -306,9 +307,10 @@ msgstr "" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -320,8 +322,8 @@ msgstr "" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -522,7 +524,7 @@ msgid "Delete" msgstr "" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." +msgid "Only Package Maintainers and Developers can delete packages." msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php @@ -563,7 +565,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -653,7 +655,7 @@ msgid "Merge" msgstr "" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." +msgid "Only Package Maintainers and Developers can merge packages." msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php @@ -711,7 +713,7 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" +msgid "Package Maintainer" msgstr "" #: html/tu.php @@ -723,7 +725,7 @@ msgid "Voting is closed for this proposal." msgstr "" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -1219,7 +1221,7 @@ msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1322,10 +1324,6 @@ msgstr "" msgid "Normal user" msgstr "" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "" @@ -1398,6 +1396,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1821,22 +1828,22 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2093,7 +2100,7 @@ msgid "Registered Users" msgstr "" #: template/stats/general_stats_table.php -msgid "Trusted Users" +msgid "Package Maintainers" msgstr "" #: template/stats/updates_table.php @@ -2279,7 +2286,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2333,3 +2340,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/it.po b/po/it.po index 436b6459..bd145ea6 100644 --- a/po/it.po +++ b/po/it.po @@ -4,23 +4,24 @@ # # Translators: # Fanfurlio Farolfi , 2021-2022 -# Giovanni Scafora , 2011-2015 +# Giovanni Scafora , 2011-2015,2022 +# Giovanni Scafora , 2022-2023 # Lorenzo Porta , 2014 # Lukas Fleischer , 2011 # mattia_b89 , 2019 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Italian (http://www.transifex.com/lfleischer/aurweb/language/it/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Giovanni Scafora , 2022-2023\n" +"Language-Team: Italian (http://app.transifex.com/lfleischer/aurweb/language/it/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: it\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Plural-Forms: nplurals=3; plural=n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #: html/404.php msgid "Page Not Found" @@ -41,12 +42,12 @@ msgstr "Le URL per clonare un repository git non sono visualizzabili nel browser #: html/404.php #, php-format msgid "To clone the Git repository of %s, run %s." -msgstr "Per clonare il reposiroty git di %s, esegui %s." +msgstr "Per clonare il repository git di %s, esegui %s." #: html/404.php #, php-format msgid "Click %shere%s to return to the %s details page." -msgstr "Clicca %squi%s per tornare alla pagina dei dettagli di %s." +msgstr "Clicca %squi%s per ritornare alla pagina dei dettagli di %s." #: html/503.php msgid "Service Unavailable" @@ -79,7 +80,7 @@ msgstr "Non hai i permessi necessari per modificare questo account." #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." -msgstr "Password non valida." +msgstr "La password non è valida." #: html/account.php msgid "Use this form to search existing accounts." @@ -136,20 +137,20 @@ msgid "Type" msgstr "Tipo" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Aggiunta di un TU" +msgid "Addition of a Package Maintainer" +msgstr "Aggiunta di un manutentore del pacchetto" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Rimozione di un TU" +msgid "Removal of a Package Maintainer" +msgstr "Rimozione di un manutentore del pacchetto" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Rimozione di un TU (inattività non dichiarata)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "Rimozione di un manutentore del pacchetto (inattività non dichiarata)" #: html/addvote.php msgid "Amendment of Bylaws" -msgstr "Modifica dello statuto" +msgstr "Modifica del regolamento" #: html/addvote.php template/tu_list.php msgid "Proposal" @@ -169,7 +170,7 @@ msgstr "Edita il commento" #: html/home.php template/header.php msgid "Dashboard" -msgstr "Cruscotto" +msgstr "Pannello" #: html/home.php template/header.php msgid "Home" @@ -202,9 +203,10 @@ msgstr "Cerca i pacchetti da me co-mantenuti" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Benvenuto in AUR! Per maggiori informazioni, leggi le %sAUR User Guidelines%s e le %sAUR TU Guidelines%s." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "Benvenuti in AUR! Prima di inviare un PKGBUILD, per maggiori informazioni, leggete le %sAUR User Guidelines%s e le %sAUR Submission Guidelines%s." #: html/home.php #, php-format @@ -218,8 +220,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "Ricorda di votare i tuoi pacchetti preferiti!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Alcuni pacchetti potrebbero essere disponibili come precompilati in [community]." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Alcuni pacchetti potrebbero essere disponibili come precompilati in [extra]." #: html/home.php msgid "DISCLAIMER" @@ -268,8 +270,8 @@ msgstr "Richiesta di rimozione di un pacchetto" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "richiesta per rimuovere un pacchetto dall'Arch User Repository. Non usare questo tipo di richiesta se un pacchetto non funziona e se può essere sistemato facilmente. Contatta il manutentore del pacchetto e, se necessario, invia una richiesta per renderlo orfano." +"the maintainer and file orphan request if necessary." +msgstr "Richiesta di rimozione di un pacchetto dall'Arch User Repository. Non utilizzare questa procedura se un pacchetto non funziona e può essere sistemato facilmente. Contattare, invece, il manutentore e, se necessario, presentare una richiesta per abbandonarlo." #: html/home.php msgid "Merge Request" @@ -311,10 +313,11 @@ msgstr "Discussione" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "La discussione generale sull'Arch User Repository (AUR) e sulla struttura dei TU avviene in %saur-general%s. Per la discussione relativa allo sviluppo dell'interfaccia web di AUR, utilizza la lista di discussione %saur-dev%s." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "La discussione generale sulla struttura dell'Arch User Repository (AUR) e dei manutentori dei pacchetti si svolge su %saur-general%s. Per le discussioni relative allo sviluppo dell'interfaccia web di AUR, utilizzare la lista di discussione %saur-dev%s." #: html/home.php msgid "Bug Reporting" @@ -325,9 +328,9 @@ msgstr "Segnalazione di un bug" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "Se trovi un bug nell'interfaccia web di AUR, invia un report al nostro %sbug tracker%s. Usa il tracker %ssolo%s per inviare i bug di AUR. Per segnalare bug inerenti alla pacchettizzazione, contatta il manutentore oppure lascia un commento nella pagina del pacchetto." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "Se si trova un bug nell'interfaccia web di AUR, si prega di compilare una segnalazione sul nostro %sbug tracker%s. Utilizzare il tracker %ssolo%s per segnalare bug nell'interfaccia web di AUR. Per segnalare bug relativi ai pacchetti, contattare il manutentore o lasciare un commento nella pagina del pacchetto." #: html/home.php msgid "Package Search" @@ -459,7 +462,7 @@ msgstr "Continua" msgid "" "If you have forgotten the user name and the primary e-mail address you used " "to register, please send a message to the %saur-general%s mailing list." -msgstr "Se hai dimenticato il nome utente e l'indirizzo email primario che hai usato per la registrazione, invia un messaggio alla mailing list %saur-general%s." +msgstr "Se hai dimenticato il nome utente e l'indirizzo email primario che hai usato per la registrazione, invia un messaggio alla lista di discussione %saur-general%s." #: html/passreset.php msgid "Enter your user name or your primary e-mail address:" @@ -527,8 +530,8 @@ msgid "Delete" msgstr "Elimina" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Solo i TU e gli sviluppatori possono eliminare i pacchetti." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "Solo i manutentori del pacchetto e gli sviluppatori possono eliminare i pacchetti." #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -568,12 +571,12 @@ msgid "Disown" msgstr "Abbandona" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "Solo i TU e gli sviluppatori possono abbandonare i pacchetti." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "Solo i manutentori e gli sviluppatori possono abbandonare un pacchetto." #: html/pkgflagcomment.php msgid "Flag Comment" -msgstr "Segnala Commento" +msgstr "Segnala il commento" #: html/pkgflag.php msgid "Flag Package Out-Of-Date" @@ -585,7 +588,7 @@ msgid "" " package version in the AUR does not match the most recent commit. Flagging " "this package should only be done if the sources moved or changes in the " "PKGBUILD are required because of recent upstream changes." -msgstr "Questo appare essere un pacchetto da VCS. Per favore %snon%s marcarlo come non aggiornato se la versione in AUR non corrisponde con il commit più recente, Questo pacchetto dovrebbe essere marcato solo se i sorgenti sono stati spostati o se sono necessari dei cambiamenti al PKGBUILD a causa delle recenti modifiche al sorgente." +msgstr "Sembra un pacchetto VCS. Fai %snon%s segnalarlo come non aggiornato, se la versione in AUR non corrisponde con il commit più recente. Questo pacchetto dovrebbe essere segnalato solo se i sorgenti vengono spostati o se sono necessarie delle modifiche al PKGBUILD a causa delle recenti modifiche al sorgente." #: html/pkgflag.php #, php-format @@ -658,8 +661,8 @@ msgid "Merge" msgstr "Unisci" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Solo i TU e gli sviluppatori possono unire i pacchetti." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "Solo i manutentori del pacchetto e gli sviluppatori possono unire i pacchetti." #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -699,12 +702,12 @@ msgstr "Usa questo modulo per creare un account." #: html/tos.php msgid "Terms of Service" -msgstr "Termini del Servizio" +msgstr "Termini di servizio" #: html/tos.php msgid "" "The following documents have been updated. Please review them carefully:" -msgstr "I seguenti documenti sono stati aggiornati. Per favore riesaminali correttamente:" +msgstr "I seguenti documenti sono stati aggiornati. Riesaminali attentamente:" #: html/tos.php #, php-format @@ -716,8 +719,8 @@ msgid "I accept the terms and conditions above." msgstr "Io accetto i termini e le condizioni di cui sopra." #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "TU" +msgid "Package Maintainer" +msgstr "Manutentore del pacchetto" #: html/tu.php msgid "Could not retrieve proposal details." @@ -728,8 +731,8 @@ msgid "Voting is closed for this proposal." msgstr "Non puoi più votare per questa proposta." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Solo i TU possono votare." +msgid "Only Package Maintainers are allowed to vote." +msgstr "Possono votare solo i manutentori del pacchetto." #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -784,7 +787,7 @@ msgstr "Può contenere solo un punto, un trattino basso o un trattino." #: lib/acctfuncs.inc.php msgid "Please confirm your new password." -msgstr "Per favore conferma la tua nuova password." +msgstr "Conferma la tua nuova password." #: lib/acctfuncs.inc.php msgid "The email address is invalid." @@ -796,7 +799,7 @@ msgstr "L'indirizzo email di scorta non è valido." #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." -msgstr "La homepage non è valida, per favore specificare l'URL HTTP(s) completo." +msgstr "La homepage non è valida, specifica l'URL HTTP(s) completo." #: lib/acctfuncs.inc.php msgid "The PGP key fingerprint is invalid." @@ -816,7 +819,7 @@ msgstr "Lingua attualmente non supportata." #: lib/acctfuncs.inc.php msgid "Timezone is not currently supported." -msgstr "Fuso orario non attualmente supportato." +msgstr "Il fuso orario non è attualmente supportato." #: lib/acctfuncs.inc.php #, php-format @@ -835,15 +838,15 @@ msgstr "La chiave pubblica SSH %s%s%s, è già in uso." #: lib/acctfuncs.inc.php msgid "The CAPTCHA is missing." -msgstr "Manca la risposta CAPTCHA." +msgstr "Manca il CAPTCHA." #: lib/acctfuncs.inc.php msgid "This CAPTCHA has expired. Please try again." -msgstr "Il CAPTCHA è scaduto, Per favore riprova." +msgstr "Il CAPTCHA è scaduto. Riprova." #: lib/acctfuncs.inc.php msgid "The entered CAPTCHA answer is invalid." -msgstr "La risposta CAPTCHA inserita non è valida." +msgstr "Il CAPTCHA inserito non è valido." #: lib/acctfuncs.inc.php #, php-format @@ -885,7 +888,7 @@ msgstr "Account sospeso" #: aurweb/routers/accounts.py msgid "You do not have permission to suspend accounts." -msgstr "Non hai il permesso per sospendere account." +msgstr "Non hai il permesso per sospendere gli account." #: lib/acctfuncs.inc.php #, php-format @@ -946,7 +949,7 @@ msgstr "Manca l'ID del commento." #: lib/pkgbasefuncs.inc.php msgid "No more than 5 comments can be pinned." -msgstr "Non possono essere inseriti più di 5 commenti." +msgstr "Non possono essere evidenziati più di 5 commenti." #: lib/pkgbasefuncs.inc.php msgid "You are not allowed to pin this comment." @@ -958,11 +961,11 @@ msgstr "Non sei autorizzato a rimuovere questo commento." #: lib/pkgbasefuncs.inc.php msgid "Comment has been pinned." -msgstr "Il commento è stato rimosso." +msgstr "Il commento è ora in evidenza." #: lib/pkgbasefuncs.inc.php msgid "Comment has been unpinned." -msgstr "I commenti sono stati rimossi." +msgstr "I commenti non sono più in evidenza." #: lib/pkgbasefuncs.inc.php lib/pkgfuncs.inc.php msgid "Error retrieving package details." @@ -1224,8 +1227,8 @@ msgstr "Sviluppatore" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "TU e sviluppatore" +msgid "Package Maintainer & Developer" +msgstr "Manutentore del pacchetto e sviluppatore" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1296,7 +1299,7 @@ msgstr "Modifica l'account di quest'utente" #: template/account_details.php msgid "List this user's comments" -msgstr "Elenca i commenti di quest'utente" +msgstr "Elenca i commenti di questo utente" #: template/account_edit_form.php #, php-format @@ -1306,7 +1309,7 @@ msgstr "Clicca %squi%s se vuoi eliminare definitivamente questo account." #: template/account_edit_form.php #, php-format msgid "Click %shere%s for user details." -msgstr "Click %squì%s per il dettagli dell'utente." +msgstr "Click %squì%s per visualizzare i dettagli dell'utente." #: template/account_edit_form.php #, php-format @@ -1327,10 +1330,6 @@ msgstr "Il tuo nome utente è il nome che userai per l'accesso. È visibile al p msgid "Normal user" msgstr "Utente normale" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "TU" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Account sospeso" @@ -1364,7 +1363,7 @@ msgstr "Indirizzo email di scorta" msgid "" "Optionally provide a secondary email address that can be used to restore " "your account in case you lose access to your primary email address." -msgstr "Puoi fornire un secondo indirizzo email che potrà essere usato per ripristinare il tuo account, nel caso tu perda l'accesso al tuo indirizzo email primario." +msgstr "Puoi fornire un secondo indirizzo email che potrà essere usato per ripristinare il tuo account, nel caso tu perdessi l'accesso al tuo indirizzo email primario." #: template/account_edit_form.php msgid "" @@ -1391,7 +1390,7 @@ msgstr "Fuso orario" msgid "" "If you want to change the password, enter a new password and confirm the new" " password by entering it again." -msgstr "Se vuoi cambiare la tua password, inseriscine una nuova e confermala inserendola di nuovo." +msgstr "Se vuoi cambiare la tua password, inseriscine una nuova e confermala digitandola di nuovo." #: template/account_edit_form.php msgid "Re-type password" @@ -1403,13 +1402,22 @@ msgid "" " the Arch User Repository." msgstr "La seguente informazione è richiesta solo se vuoi inviare i pacchetti nell'Arch User Repository." +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "Specifica più chiavi SSH separate da una nuova riga, le linee vuote saranno ignorate." + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "Nascondi commenti eliminati" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "Chiave pubblica SSH" #: template/account_edit_form.php msgid "Notification settings" -msgstr "Impostazioni notifiche" +msgstr "Impostazioni delle notifiche" #: template/account_edit_form.php msgid "Notify of new comments" @@ -1421,11 +1429,11 @@ msgstr "Notifica degli aggiornamenti dei pacchetti" #: template/account_edit_form.php msgid "Notify of ownership changes" -msgstr "Notifica cambiamenti di proprietà" +msgstr "Notifica dei cambiamenti di proprietà" #: template/account_edit_form.php msgid "To confirm the profile changes, please enter your current password:" -msgstr "Per confermare le modifiche al profilo, per favore inserisci la tua password:" +msgstr "Per confermare le modifiche al profilo, inserisci la tua password:" #: template/account_edit_form.php msgid "Your current password" @@ -1499,7 +1507,7 @@ msgstr "Salva" #: template/flag_comment.php #, php-format msgid "Flagged Out-of-Date Comment: %s" -msgstr "Commento per la marcatura come Non Aggiornato: %s" +msgstr "Commento per la segnalazione come Non Aggiornato: %s" #: template/flag_comment.php #, php-format @@ -1513,7 +1521,7 @@ msgstr "%s%s%s non è segnalato come non aggiornato." #: template/flag_comment.php msgid "Return to Details" -msgstr "Ritorna ai Dettagli" +msgstr "Ritorna ai dettagli" #: template/footer.php #, php-format @@ -1583,6 +1591,7 @@ msgid "%d pending request" msgid_plural "%d pending requests" msgstr[0] "%d richiesta in attesa" msgstr[1] "%d richieste in attesa" +msgstr[2] "%d richieste in attesa" #: template/pkgbase_actions.php msgid "Adopt Package" @@ -1654,7 +1663,7 @@ msgstr "Aggiungi un commento" msgid "" "Git commit identifiers referencing commits in the AUR package repository and" " URLs are converted to links automatically." -msgstr "Gli identificatori dei commit Git nel repository dei pacchetti AUR e le URL vengono convertite automaticamente in link." +msgstr "Gli identificatori dei commit Git nel repository dei pacchetti AUR e le URL vengono convertiti automaticamente in link." #: template/pkg_comment_form.php #, php-format @@ -1663,7 +1672,7 @@ msgstr "La %ssintassi Markdown%s è parzialmente supportata." #: template/pkg_comments.php msgid "Pinned Comments" -msgstr "Elimina i commenti" +msgstr "Commenti in evidenza" #: template/pkg_comments.php msgid "Latest Comments" @@ -1676,7 +1685,7 @@ msgstr "Commenti per" #: template/pkg_comments.php #, php-format msgid "%s commented on %s" -msgstr "%s ha commentato su %s" +msgstr "%s ha commentato il %s" #: template/pkg_comments.php #, php-format @@ -1686,27 +1695,27 @@ msgstr "Commento anonimo su %s" #: template/pkg_comments.php #, php-format msgid "Commented on package %s on %s" -msgstr "Ha commentato sul pacchetto %s su %s" +msgstr "Ha commentato sul pacchetto %s il %s" #: template/pkg_comments.php #, php-format msgid "deleted on %s by %s" -msgstr "Eliminato su %s da %s" +msgstr "eliminato il %s da %s" #: template/pkg_comments.php #, php-format msgid "deleted on %s" -msgstr "cancellato su %s" +msgstr "eliminato il %s" #: template/pkg_comments.php #, php-format msgid "edited on %s by %s" -msgstr "modificato su %s da %s" +msgstr "modificato il %s da %s" #: template/pkg_comments.php #, php-format msgid "edited on %s" -msgstr "modificato su %s" +msgstr "modificato il %s" #: template/pkg_comments.php msgid "Undelete comment" @@ -1826,26 +1835,26 @@ msgstr "Unisci con" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "Inserendo una richiesta di cancellazione, stai chiedendo ad un Trusted User di cancellare il pacchetto base. Questo tipo di richiesta dovrebbe essere usato per duplicati, software abbandonati dall'autore, sotware illegalmente distribuiti o pacchetti irreparabili." +msgstr "Inviando una richiesta di cancellazione, si chiede al manutentore del pacchetto di eliminare la base del pacchetto. Questo tipo di richiesta dovrebbe essere usata per i duplicati, per il software abbandonato dall'upstream, per i pacchetti illegali e per quelli non più funzionanti." #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "Inserendo una richiesta di unione, stai chiedendo ad un Trusted User di cancellare il pacchetto base e trasferire i suoi voti e commenti su un altro pacchetto base. Unire due pacchetti non ha effetto sul corrispondente repository Git. Assicurati di aggiornare lo storico Git del pacchetto di destinazione." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "Inviando una richiesta di unione, si chiede al manutentore di eliminare un pacchetto e di trasferire i suoi voti ed i commenti ad un altro pacchetto. L'unione di un pacchetto non influisce sui repository Git corrispondenti. Assicuratevi di aggiornare voi stessi la cronologia Git del pacchetto di destinazione." #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "Inserendo una richiesta di abbandono, stai chiedendo ad un Trusted User di rimuovere la proprietà del pacchetto base. Per favore procedi soltanto se il pacchetto necessita di manutenzione, il manutentore attuale non risponde, e hai già provato a contattarlo precedentemente." +msgstr "Inviando una richiesta di abbandono di un pacchetto, si chiede ad un manutentore di abbandonarlo. Si prega di farlo solo se il pacchetto necessita di un intervento del manutentore, se il manutentore è irreperibile e se si è già provato a contattarlo in precedenza." #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -1857,6 +1866,7 @@ msgid "%d package request found." msgid_plural "%d package requests found." msgstr[0] "È stato trovato %d pacchetto." msgstr[1] "Sono stati trovati %d pacchetti." +msgstr[2] "Sono stati trovati %d pacchetti." #: template/pkgreq_results.php template/pkg_search_results.php #, php-format @@ -1881,6 +1891,7 @@ msgid "~%d day left" msgid_plural "~%d days left" msgstr[0] "~%d giorno rimanente" msgstr[1] "~%d giorni rimanenti" +msgstr[2] "~%d giorni rimanenti" #: template/pkgreq_results.php #, php-format @@ -1888,6 +1899,7 @@ msgid "~%d hour left" msgid_plural "~%d hours left" msgstr[0] "~%d ora rimanente" msgstr[1] "~%d ore rimanenti" +msgstr[2] "~%d ore rimanenti" #: template/pkgreq_results.php msgid "<1 hour left" @@ -2016,6 +2028,7 @@ msgid "%d package found." msgid_plural "%d packages found." msgstr[0] "È stato trovato %d pacchetto." msgstr[1] "Sono stati trovati %d pacchetti." +msgstr[2] "Sono stati trovati %d pacchetti." #: template/pkg_search_results.php msgid "Version" @@ -2026,7 +2039,7 @@ msgstr "Versione" msgid "" "Popularity is calculated as the sum of all votes with each vote being " "weighted with a factor of %.2f per day since its creation." -msgstr "La popolarità è calcolata come somma di tutti i voti pesati con un fattore di %.2f al giorno, dalla sua creazione." +msgstr "La popolarità è calcolata come somma di tutti i voti ponderati con un fattore di %.2f al giorno dalla sua creazione." #: template/pkg_search_results.php template/tu_details.php #: template/tu_list.php @@ -2098,8 +2111,8 @@ msgid "Registered Users" msgstr "Utenti registrati" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "TU" +msgid "Package Maintainers" +msgstr "Manutentori del pacchetto" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2172,7 +2185,7 @@ msgstr "Precedente" #: scripts/notify.py msgid "AUR Password Reset" -msgstr "Ripristino Password di AUR" +msgstr "Ripristino della password di AUR" #: scripts/notify.py #, python-brace-format @@ -2180,18 +2193,18 @@ msgid "" "A password reset request was submitted for the account {user} associated " "with your email address. If you wish to reset your password follow the link " "[1] below, otherwise ignore this message and nothing will happen." -msgstr "È stata inviata una richiesta per ripristinare la password dell'account {user} associato al tuo indirizzo e-mail. Se desideri ripristinare la tua password, clicca sul link [1] sottostante, altrimenti ignora questo messaggio e non succederà nulla." +msgstr "È stata inviata una richiesta per ripristinare la password dell'account {user} associato al tuo indirizzo email. Se desideri ripristinare la tua password, clicca sul link [1] sottostante, altrimenti ignora questo messaggio e non succederà nulla." #: scripts/notify.py msgid "Welcome to the Arch User Repository" -msgstr "Benvenuto nel Arch User Repository" +msgstr "Benvenuto nell' Arch User Repository" #: scripts/notify.py msgid "" "Welcome to the Arch User Repository! In order to set an initial password for" " your new account, please click the link [1] below. If the link does not " "work, try copying and pasting it into your browser." -msgstr "Benvenuto nel Arch User Repository! Per impostare una password iniziale per il tuo nuovo account, per favore segui il collegamento [1] sottostante. Se il collegamento non funziona, prova a copiarlo e incollarlo nel tuo browser." +msgstr "Benvenuto nell' Arch User Repository! Per impostare una password iniziale per il tuo nuovo account, clicca sul link [1] sottostante. Se il link non funzionasse, prova a copiarlo e ad incollarlo nella barra degli indirizzi del tuo browser." #: scripts/notify.py #, python-brace-format @@ -2208,12 +2221,12 @@ msgstr "{user} [1] ha commentato su {pkgbase} [2]:" msgid "" "If you no longer wish to receive notifications about this package, please go" " to the package page [2] and select \"{label}\"." -msgstr "Se non vuoi più ricevere notifiche su questo pacchetto, per favore vai alla pagina del pacchetto [2] e seleziona \"{label}\"." +msgstr "Se non vuoi più ricevere notifiche su questo pacchetto, vai alla pagina del pacchetto [2] e seleziona \"{label}\"." #: scripts/notify.py #, python-brace-format msgid "AUR Package Update: {pkgbase}" -msgstr "Aggiornamento pachetto base: {pkgbase}" +msgstr "Aggiornamento del pacchetto base: {pkgbase}" #: scripts/notify.py #, python-brace-format @@ -2223,7 +2236,7 @@ msgstr "{user} [1] ha inviato un nuovo commit su {pkgbase} [2]." #: scripts/notify.py #, python-brace-format msgid "AUR Out-of-date Notification for {pkgbase}" -msgstr "Notifica AUR per pacchetto {pkgbase} non aggiornato" +msgstr "Notifica AUR per il pacchetto {pkgbase} non aggiornato" #: scripts/notify.py #, python-brace-format @@ -2233,7 +2246,7 @@ msgstr "Il tuo pacchetto {pkgbase} [1] è stato marcato come non aggiornato dall #: scripts/notify.py #, python-brace-format msgid "AUR Ownership Notification for {pkgbase}" -msgstr "Notifica AUR di proprietà per pacchetto {pkgbase} " +msgstr "Notifica AUR di proprietà del pacchetto {pkgbase} " #: scripts/notify.py #, python-brace-format @@ -2248,7 +2261,7 @@ msgstr "Il pacchetto {pkgbase} [1] è stato abbandonato da {user} [2]." #: scripts/notify.py #, python-brace-format msgid "AUR Co-Maintainer Notification for {pkgbase}" -msgstr "Notifica AUR di co-manutenzione per pacchetto {pkgbase} " +msgstr "Notifica AUR di co-manutenzione per il pacchetto {pkgbase} " #: scripts/notify.py #, python-brace-format @@ -2272,7 +2285,7 @@ msgid "" "\n" "-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." -msgstr "{user} [1] ha unito {old} [2] in {new} [3].\n\n-- \nSe non vuoi più ricevere notifiche sul nuovo pacchetto, per favore vai a [3] e clicca su \"{label}\"." +msgstr "{user} [1] ha unito {old} [2] in {new} [3].\n\n-- \nSe non desideri più ricevere notifiche sul nuovo pacchetto, vai a [3] e clicca su \"{label}\"." #: scripts/notify.py #, python-brace-format @@ -2280,31 +2293,31 @@ msgid "" "{user} [1] deleted {pkgbase} [2].\n" "\n" "You will no longer receive notifications about this package." -msgstr "{user} [1] ha eliminato {pkgbase} [2].\n\nNon riceverai più notifiche su questo pacchetto." +msgstr "{user} [1] ha eliminato {pkgbase} [2].\n\nNon riceverai più notifiche per questo pacchetto." #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" -msgstr "Promemoria per voto TU: Proposta {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" +msgstr "Promemoria per il voto del manutentore dei pacchetti: proposta {id}" #: scripts/notify.py #, python-brace-format msgid "" "Please remember to cast your vote on proposal {id} [1]. The voting period " "ends in less than 48 hours." -msgstr "Per favore ricordati di votare sulla proposta {id} [1]. La finestra di voto si chiude fra meno di 48 ore." +msgstr "Ricordati di votare la proposta di {id} [1]. La finestra di voto si chiude fra meno di 48 ore." #: aurweb/routers/accounts.py msgid "Invalid account type provided." -msgstr "Tipo di account non valido." +msgstr "L' account fornito non è valido." #: aurweb/routers/accounts.py msgid "You do not have permission to change account types." -msgstr "Non hai il permesso per cambiare il tipo di account." +msgstr "Non hai il permesso per modificare il tipo di account." #: aurweb/routers/accounts.py msgid "You do not have permission to change this user's account type to %s." -msgstr "Non hai il permesso per cambiare il tipo di account di questo utente in %s." +msgstr "Non hai il permesso per modificare il tipo di account di questo utente in %s." #: aurweb/packages/requests.py msgid "No due existing orphan requests to accept for %s." @@ -2322,19 +2335,51 @@ msgstr "Si è verificato un errore irreversibile." msgid "" "Details have been logged and will be reviewed by the postmaster posthaste. " "We apologize for any inconvenience this may have caused." -msgstr "I dettagli sono stati registrati e verranno visionati da postmaster velocemente. Ci scusiamo per l'inconvenienza che questo possa aver causato." +msgstr "I dettagli sono stati registrati e verranno visionati al più presto dal postmaster. Ci scusiamo per gli eventuali disagi causati." #: aurweb/scripts/notify.py msgid "AUR Server Error" -msgstr "Errore server AUR" +msgstr "Errore del server di AUR" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "Commenti relativi alla richiesta di chiusura del pacchetto..." #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." -msgstr "" +msgstr "Questa azione chiuderà tutte le richieste in sospeso dei pacchetti ad essa correlate. Se %scommenti%s vengono omessi, verrà generato automaticamente un commento di chiusura." + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "assegnato" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "Mostra altri %d" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "dipendenze" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "L'account non è stato eliminato, selezionare la casella di conferma." + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "Annulla" + +#: templates/requests.html +msgid "Package name" +msgstr "Nome del pacchetto" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "Si noti che, se si nasconde il proprio indirizzo e-mail, esso finirà nell'elenco BCC per qualsiasi notifica di richiesta. Nel caso in cui qualcuno risponda a queste notifiche, non si riceverà un'e-mail. Tuttavia, le risposte sono tipicamente inviate alla lista di discussione e saranno, quindi, visibili nell'archivio." diff --git a/po/ja.po b/po/ja.po index 55d056bf..4d356963 100644 --- a/po/ja.po +++ b/po/ja.po @@ -10,11 +10,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Japanese (http://www.transifex.com/lfleischer/aurweb/language/ja/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: kusakata, 2013-2018,2020-2022\n" +"Language-Team: Japanese (http://app.transifex.com/lfleischer/aurweb/language/ja/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -135,16 +135,16 @@ msgid "Type" msgstr "タイプ" #: html/addvote.php -msgid "Addition of a TU" -msgstr "TU の追加" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "TU の削除" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "TU の削除 (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -201,9 +201,10 @@ msgstr "共同メンテしているパッケージを検索" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "AUR にようこそ!AUR についての詳しい情報は %sAUR User Guidelines%s や %sAUR TU Guidelines%s を読んで下さい。" +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -217,8 +218,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "お気に入りのパッケージに投票しましょう!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "パッケージがバイナリとして [community] で提供されることになるかもしれません。" +msgid "Some packages may be provided as binaries in [extra]." +msgstr "パッケージがバイナリとして [extra] で提供されることになるかもしれません。" #: html/home.php msgid "DISCLAIMER" @@ -267,8 +268,8 @@ msgstr "削除リクエスト" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "Arch User Repository からパッケージを削除するようにリクエストします。パッケージが壊れていて、その不具合を簡単に修正できるような場合、このリクエストは使わないで下さい。代わりに、パッケージのメンテナに連絡したり、必要に応じて孤児リクエストを送りましょう。" +"the maintainer and file orphan request if necessary." +msgstr "" #: html/home.php msgid "Merge Request" @@ -310,10 +311,11 @@ msgstr "議論" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "Arch User Repository (AUR) や Trusted User に関する一般的な議論は %saur-general%s で行って下さい。AUR ウェブインターフェイスの開発に関しては、%saur-dev%s メーリングリストを使用します。" +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -324,9 +326,9 @@ msgstr "バグレポート" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "AUR のウェブインターフェイスにバグを発見した時は%sバグトラッカー%sにバグを報告してください。トラッカーを使って報告できるバグは AUR ウェブインターフェイスのバグ%sだけ%sです。パッケージのバグを報告するときはパッケージのメンテナに連絡するか該当するパッケージのページにコメントを投稿してください。" +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "" #: html/home.php msgid "Package Search" @@ -526,8 +528,8 @@ msgid "Delete" msgstr "削除" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Trusted User と開発者だけがパッケージを削除できます。" +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -567,8 +569,8 @@ msgid "Disown" msgstr "放棄" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "Trusted User と開発者だけがパッケージを孤児にできます。" +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -657,8 +659,8 @@ msgid "Merge" msgstr "マージ" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Trusted User と開発者だけがパッケージをマージできます。" +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -715,8 +717,8 @@ msgid "I accept the terms and conditions above." msgstr "私は上記の利用規約を承認します。" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Trusted User" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -727,8 +729,8 @@ msgid "Voting is closed for this proposal." msgstr "この提案への投票は締め切られています。" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Trusted User だけが投票できます。" +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -1223,8 +1225,8 @@ msgstr "開発者" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "Trusted User & 開発者" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1326,10 +1328,6 @@ msgstr "ユーザー名はログインするときに使用する名前です。 msgid "Normal user" msgstr "ノーマルユーザー" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Trusted user" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "休眠アカウント" @@ -1402,6 +1400,15 @@ msgid "" " the Arch User Repository." msgstr "以下の情報は Arch User Repository にパッケージを送信したい場合にのみ必要になります。" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "SSH 公開鍵" @@ -1824,26 +1831,26 @@ msgstr "マージ" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "削除リクエストを送信することで、Trusted User にパッケージベースの削除を要求できます。削除リクエストを使用するケース: パッケージの重複や、上流によってソフトウェアの開発が放棄された場合、違法なパッケージ、あるいはパッケージがどうしようもなく壊れてしまっている場合など。" +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "マージリクエストを送信することで、パッケージベースを削除して\n投票数とコメントを他のパッケージベースに移動することを Trusted User に要求できます。パッケージのマージは Git リポジトリに影響を与えません。マージ先のパッケージの Git 履歴は自分で更新してください。" +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "孤児リクエストを送信することで、パッケージベースの所有権が放棄されるように Trusted User に要求できます。パッケージにメンテナが何らかの手を加える必要があり、現在のメンテナが行方不明で、メンテナに連絡を取ろうとしても返答がない場合にのみ、リクエストを送信してください。" +msgstr "" #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -2092,8 +2099,8 @@ msgid "Registered Users" msgstr "登録ユーザー" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Trusted User" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2278,8 +2285,8 @@ msgstr "{user} [1] は {pkgbase} [2] を削除しました。\n\nこのパッケ #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" -msgstr "TU 投票リマインダー: 提案 {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" +msgstr "" #: scripts/notify.py #, python-brace-format @@ -2325,10 +2332,42 @@ msgstr "AUR サーバーエラー" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "関連するパッケージリクエストの取り消しコメント..." #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." +msgstr "このアクションは関連するパッケージリクエストをすべて取り消します。%sコメント%sを省略した場合、自動的にコメントが生成されます。" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." msgstr "" diff --git a/po/ko.po b/po/ko.po index 808ffe27..b6d8de3b 100644 --- a/po/ko.po +++ b/po/ko.po @@ -6,11 +6,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Korean (http://www.transifex.com/lfleischer/aurweb/language/ko/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Korean (http://app.transifex.com/lfleischer/aurweb/language/ko/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -131,15 +131,15 @@ msgid "Type" msgstr "" #: html/addvote.php -msgid "Addition of a TU" +msgid "Addition of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU" +msgid "Removal of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" msgstr "" #: html/addvote.php @@ -197,8 +197,9 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." msgstr "" #: html/home.php @@ -213,7 +214,7 @@ msgid "Remember to vote for your favourite packages!" msgstr "" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." +msgid "Some packages may be provided as binaries in [extra]." msgstr "" #: html/home.php @@ -263,7 +264,7 @@ msgstr "" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -306,9 +307,10 @@ msgstr "" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -320,8 +322,8 @@ msgstr "" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -522,7 +524,7 @@ msgid "Delete" msgstr "" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." +msgid "Only Package Maintainers and Developers can delete packages." msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php @@ -563,7 +565,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -653,7 +655,7 @@ msgid "Merge" msgstr "" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." +msgid "Only Package Maintainers and Developers can merge packages." msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php @@ -711,7 +713,7 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" +msgid "Package Maintainer" msgstr "" #: html/tu.php @@ -723,7 +725,7 @@ msgid "Voting is closed for this proposal." msgstr "" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -1219,7 +1221,7 @@ msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1322,10 +1324,6 @@ msgstr "" msgid "Normal user" msgstr "" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "" @@ -1398,6 +1396,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1820,22 +1827,22 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2088,7 +2095,7 @@ msgid "Registered Users" msgstr "" #: template/stats/general_stats_table.php -msgid "Trusted Users" +msgid "Package Maintainers" msgstr "" #: template/stats/updates_table.php @@ -2274,7 +2281,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2328,3 +2335,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/lt.po b/po/lt.po index d126f193..f9e7f258 100644 --- a/po/lt.po +++ b/po/lt.po @@ -6,11 +6,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Lithuanian (http://www.transifex.com/lfleischer/aurweb/language/lt/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Lithuanian (http://app.transifex.com/lfleischer/aurweb/language/lt/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -131,15 +131,15 @@ msgid "Type" msgstr "" #: html/addvote.php -msgid "Addition of a TU" +msgid "Addition of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU" +msgid "Removal of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" msgstr "" #: html/addvote.php @@ -197,8 +197,9 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." msgstr "" #: html/home.php @@ -213,7 +214,7 @@ msgid "Remember to vote for your favourite packages!" msgstr "" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." +msgid "Some packages may be provided as binaries in [extra]." msgstr "" #: html/home.php @@ -263,7 +264,7 @@ msgstr "" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -306,9 +307,10 @@ msgstr "" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -320,8 +322,8 @@ msgstr "" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -522,7 +524,7 @@ msgid "Delete" msgstr "" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." +msgid "Only Package Maintainers and Developers can delete packages." msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php @@ -563,7 +565,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -653,7 +655,7 @@ msgid "Merge" msgstr "" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." +msgid "Only Package Maintainers and Developers can merge packages." msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php @@ -711,7 +713,7 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" +msgid "Package Maintainer" msgstr "" #: html/tu.php @@ -723,7 +725,7 @@ msgid "Voting is closed for this proposal." msgstr "" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -1219,7 +1221,7 @@ msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1322,10 +1324,6 @@ msgstr "" msgid "Normal user" msgstr "" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "" @@ -1398,6 +1396,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1823,22 +1830,22 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2103,7 +2110,7 @@ msgid "Registered Users" msgstr "" #: template/stats/general_stats_table.php -msgid "Trusted Users" +msgid "Package Maintainers" msgstr "" #: template/stats/updates_table.php @@ -2289,7 +2296,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2343,3 +2350,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/nb.po b/po/nb.po index 1cc090f1..f67571d7 100644 --- a/po/nb.po +++ b/po/nb.po @@ -12,11 +12,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Norwegian Bokmål (http://www.transifex.com/lfleischer/aurweb/language/nb/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Alexander F. Rødseth , 2015,2017-2019\n" +"Language-Team: Norwegian Bokmål (http://app.transifex.com/lfleischer/aurweb/language/nb/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -137,16 +137,16 @@ msgid "Type" msgstr "Type" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Utnevnelse av TU" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Fjerning av TU" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Fjerning av TU (inaktiv uten å si i fra)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -203,9 +203,10 @@ msgstr "Søk etter pakker jeg er med på å vedlikeholde" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Velkommen til AUR! Vennligst les %sAUR Brukerveiledning%s og %sAUR TU Veiledning%s for mer informasjon." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -219,8 +220,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "Husk å stemme på dine favorittpakker!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Noen pakker finnes som binærfiler i [community]." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Noen pakker finnes som binærfiler i [extra]." #: html/home.php msgid "DISCLAIMER" @@ -269,8 +270,8 @@ msgstr "Forespør sletting" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "Forespør at pakken fjernes fra Arch User Repository. Ikke gjør dette hvis pakken er ødelagt og lett kan fikses. Ta kontakt med vedlikeholderen i stedet, eller forespør at pakken gjøres eierløs." +"the maintainer and file orphan request if necessary." +msgstr "" #: html/home.php msgid "Merge Request" @@ -312,10 +313,11 @@ msgstr "Diskusjon" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "Generell diskusjon rundt Arch sitt brukerstyrte pakkebibliotek (AUR) og strukturen rundt betrodde brukere, foregår på %saur-general%s. For diskusjoner relatert til utviklingen av AUR web-grensesnittet, bruk %saur-dev%s e-postlisten." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -326,9 +328,9 @@ msgstr "Feilrapportering" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "Vennligst fyll ut en feilrapport i %sfeilrapporteringssystemet%s dersom du finner en feil i AUR sitt web-grensesnitt. Bruk denne %skun%s til å rapportere feil som gjelder AUR sitt web-grensesnitt. For å rapportere feil med pakker, kontakt personen som vedlikeholder pakken eller legg igjen en kommentar på siden til den aktuelle pakken." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "" #: html/home.php msgid "Package Search" @@ -528,8 +530,8 @@ msgid "Delete" msgstr "Slett" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Bare betrodde brukere og utviklere kan slette pakker." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -569,8 +571,8 @@ msgid "Disown" msgstr "Gjør eierløs" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "Bare betrodde brukere og Arch utviklere kan gjøre pakker eierløse." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -659,8 +661,8 @@ msgid "Merge" msgstr "Slå sammen" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Bare betrodde brukere og utviklere kan slå sammen pakker." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -717,8 +719,8 @@ msgid "I accept the terms and conditions above." msgstr "Jeg godtar betingelsene ovenfor." #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Betrodd bruker" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -729,8 +731,8 @@ msgid "Voting is closed for this proposal." msgstr "Avstemningen er ferdig for dette forslaget." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Bare betrodde brukere har stemmerett." +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -1225,8 +1227,8 @@ msgstr "Utvikler" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "Betrodd bruker & Utvikler" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1328,10 +1330,6 @@ msgstr "Brukernavnet er navnet du vil bruke for å logge inn med. Det er synlig msgid "Normal user" msgstr "Vanlig bruker" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Betrodd bruker" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Konto suspendert" @@ -1404,6 +1402,15 @@ msgid "" " the Arch User Repository." msgstr "Følgende informasjon behøves bare hvis du har tenkt til å sende inn pakker til Arch sitt brukerstyrte pakkebibliotek." +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "Offentlig SSH-nøkkel" @@ -1827,26 +1834,26 @@ msgstr "Flett med" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "Ved å sende inn en forespørsel om sletting spør du en betrodd bruker om å slette pakken. Slike forespørsler bør brukes om duplikater, forlatt programvare samt ulovlige eller pakker som er så ødelagte at de ikke lenger kan fikses." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "Ved å sende inn en forespørsel om sammenslåing spør du en betrodd bruker om å slette pakken. Stemmer og kommentarer vil bli overført til en annen pakke. Å slå sammen en pakke har ingen effekt på korresponderende Git repo. Pass på å oppdatere Git historikken til målpakken selv." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "Ved å sende inn en forespørsel om å gjøre en pakke eierløs spør du en betrodd bruker om å utføre dette. Vennligst bare send inn forespørselen dersom pakken trenger vedlikehold, nåværende vedlikeholder er fraværende og du allerede har prøvd å kontakte den som vedlikeholder pakken." +msgstr "" #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -2099,8 +2106,8 @@ msgid "Registered Users" msgstr "Registrerte brukere" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Betrodde brukere" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2285,8 +2292,8 @@ msgstr "{user} [1] slettet {pkgbase} [2].\n\nDu vil ikke få flere beskjeder om #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" -msgstr "TU avstemningspåminnelse: forslag {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" +msgstr "" #: scripts/notify.py #, python-brace-format @@ -2339,3 +2346,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/nb_NO.po b/po/nb_NO.po index 74af6936..e840e869 100644 --- a/po/nb_NO.po +++ b/po/nb_NO.po @@ -8,11 +8,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Norwegian Bokmål (Norway) (http://www.transifex.com/lfleischer/aurweb/language/nb_NO/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Kim Nordmo , 2017,2019\n" +"Language-Team: Norwegian Bokmål (Norway) (http://app.transifex.com/lfleischer/aurweb/language/nb_NO/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -133,15 +133,15 @@ msgid "Type" msgstr "Type" #: html/addvote.php -msgid "Addition of a TU" +msgid "Addition of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU" +msgid "Removal of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" msgstr "" #: html/addvote.php @@ -199,8 +199,9 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." msgstr "" #: html/home.php @@ -215,7 +216,7 @@ msgid "Remember to vote for your favourite packages!" msgstr "" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." +msgid "Some packages may be provided as binaries in [extra]." msgstr "" #: html/home.php @@ -265,7 +266,7 @@ msgstr "" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -308,9 +309,10 @@ msgstr "Diskusjon" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -322,8 +324,8 @@ msgstr "" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -524,7 +526,7 @@ msgid "Delete" msgstr "" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." +msgid "Only Package Maintainers and Developers can delete packages." msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php @@ -565,7 +567,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -655,7 +657,7 @@ msgid "Merge" msgstr "" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." +msgid "Only Package Maintainers and Developers can merge packages." msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php @@ -713,8 +715,8 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Betrodd bruker" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -725,7 +727,7 @@ msgid "Voting is closed for this proposal." msgstr "Stemming er avsluttet for dette forslaget." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -1221,7 +1223,7 @@ msgstr "Utvikler" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1324,10 +1326,6 @@ msgstr "" msgid "Normal user" msgstr "Vanlig bruker" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Betrodd bruker" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Konto suspendert" @@ -1400,6 +1398,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1823,22 +1830,22 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2095,7 +2102,7 @@ msgid "Registered Users" msgstr "" #: template/stats/general_stats_table.php -msgid "Trusted Users" +msgid "Package Maintainers" msgstr "" #: template/stats/updates_table.php @@ -2281,7 +2288,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2335,3 +2342,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/nl.po b/po/nl.po index 282b5b40..d8df5765 100644 --- a/po/nl.po +++ b/po/nl.po @@ -13,11 +13,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Dutch (http://www.transifex.com/lfleischer/aurweb/language/nl/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Heimen Stoffels , 2021-2022\n" +"Language-Team: Dutch (http://app.transifex.com/lfleischer/aurweb/language/nl/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -138,16 +138,16 @@ msgid "Type" msgstr "Type" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Toevoeging van een TU" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Verwijdering van een TU" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Verwijdering van een TU (onverklaarbare inactiviteit)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -204,9 +204,10 @@ msgstr "Zoeken naar pakketten die ik mede onderhoud" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Welkom op AUR! Bekijk voor meer informatie de %sAUR-gebruikersrichtlijnen%s en %sAUR-ontwikkelaarsrichtlijnen%s." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -220,8 +221,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "Vergeet niet te stemmen op uw favoriete pakketten!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Sommige pakketten in [community] kunnen worden aangeleverd als uitvoerbare bestanden." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Sommige pakketten in [extra] kunnen worden aangeleverd als uitvoerbare bestanden." #: html/home.php msgid "DISCLAIMER" @@ -270,8 +271,8 @@ msgstr "Verwijderingsverzoek" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "Verzoek om een pakket uit de Arch User Repository te laten verwijderen. Gebruik dit niet als een pakket niet naar behoren functioneert en eenvoudig gerepareerd kan worden. Neem in dat geval contact op met de eigenaar en open zo nodig een onteigeningsverzoek." +"the maintainer and file orphan request if necessary." +msgstr "" #: html/home.php msgid "Merge Request" @@ -313,10 +314,11 @@ msgstr "Discussiëren" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "Algemene discussies met betrekking tot de Arch User Repository (AUR) en de opzet van Trusted Users vinden plaats op %saur-general%s. Discussies met betrekking tot de ontwikkeling van de AUR-webapp vinden plaats op de %saur-dev%s-mailinglijst." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -327,9 +329,9 @@ msgstr "Bugmeldingen" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "Als u een bug tegenkomt in de AUR-webapp, open dan een ticket op onze %sbugtracker%s. Gebruik de bugtracker %salléén%s om bugs over AUR te melden. Als u fouten tegenkomt in een pakketsamenstelling, meld dit dan aan de eigenaar of laat een opmerking achter op de bijbehorende pakketpagina." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "" #: html/home.php msgid "Package Search" @@ -529,8 +531,8 @@ msgid "Delete" msgstr "Verwijderen" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Alleen Trusted Users en Developers kunnen pakketten verwijderen." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -570,8 +572,8 @@ msgid "Disown" msgstr "Onteigenen" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "Alleen Trusted Users en Developers kunnen pakketten onteigenen." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -660,8 +662,8 @@ msgid "Merge" msgstr "Samenvoegen" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Alleen Trusted Users en Developers kunnen pakketten samenvoegen." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -718,8 +720,8 @@ msgid "I accept the terms and conditions above." msgstr "Ik ga akkoord met de algemene voorwaarden." #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Trusted User" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -730,8 +732,8 @@ msgid "Voting is closed for this proposal." msgstr "U kunt niet meer stemmen op dit voorstel." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Alleen Trusted Users zijn bevoegd om te stemmen." +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -1226,8 +1228,8 @@ msgstr "Ontwikkelaar" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "Trusted User en ontwikkelaar" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1329,10 +1331,6 @@ msgstr "Uw gebruikersnaam gebruikt u om in te loggen. Deze naam is openbaar, zel msgid "Normal user" msgstr "Normale gebruiker" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Trusted User" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Account geschorst" @@ -1405,6 +1403,15 @@ msgid "" " the Arch User Repository." msgstr "De volgende informatie is alleen verplicht als u pakketten aan de Arch User Repository wilt toevoegen." +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "Publieke ssh-sleutel" @@ -1828,26 +1835,26 @@ msgstr "Samenvoegen met" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "Als u een verwijderingsverzoek doet, vraagt u aan een zogeheten ‘Trusted User’ om het basispakket te verwijderen. Dit verzoek is bedoeld om duplicaten, niet-onderhouden (door upstream), illegale en onherstelbare pakketten te verwijderen." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "Als u een samenvoegingsverzoek doet, vraagt u aan een zogeheten ‘Trusted User’ om het basispakket te verwijderen en de bijbehorende opmerkingen en stemmen over te zetten naar een ander basispakket. Dit heeft geen invloed op de bijbehorende git-repo's, maar u dient wél zelf de git-geschiedenis van het doelpakket bij te werken." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "Als u een onteigeningsverzoek doet, vraagt u aan een zogeheten ‘Trusted User’ om het basispakket te onteigenen. Doe dit alleen als een pakket dringend onderhoud nodig heeft, maar de eigenaar niet thuis geeft, ook niet na een persoonlijk verzoek." +msgstr "" #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -2100,8 +2107,8 @@ msgid "Registered Users" msgstr "Geregistreerde gebruikers" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Trusted Users" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2286,8 +2293,8 @@ msgstr "{user} [1] heeft {pkgbase} [2] verwijderd.\n\n\nU ontvangt geen meldinge #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" -msgstr "TU-stemherinnering aangaande het voorstel ‘{id}’" +msgid "Package Maintainer Vote Reminder: Proposal {id}" +msgstr "" #: scripts/notify.py #, python-brace-format @@ -2333,10 +2340,42 @@ msgstr "AUR-serverfout" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "Gerelateerde pakketverzoekreacties…" #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." +msgstr "Met deze actie sluit u elk gerelateerd openstaand verzoek. Als %s reacties%s genegeerd worden, dan wordt er een automatische afsluitreactie geplaatst." + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." msgstr "" diff --git a/po/pl.po b/po/pl.po index 4856f22b..e68fa533 100644 --- a/po/pl.po +++ b/po/pl.po @@ -13,17 +13,17 @@ # marcin mikołajczak , 2017 # Michal T , 2016 # Nuc1eoN , 2014 -# Piotr Strębski , 2017-2018 +# Piotr Strębski , 2017-2018,2022 # Piotr Strębski , 2013-2016 # Przemyslaw Ka. , 2021 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Polish (http://www.transifex.com/lfleischer/aurweb/language/pl/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Piotr Strębski , 2017-2018,2022\n" +"Language-Team: Polish (http://app.transifex.com/lfleischer/aurweb/language/pl/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -144,16 +144,16 @@ msgid "Type" msgstr "Rodzaj" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Dodanie ZU" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Usunięcie ZU" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Usunięcie ZU (niezadeklarowana nieaktywność)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -210,9 +210,10 @@ msgstr "Wyszukaj pakiety, które współ-utrzymuję" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Witamy w AUR! Aby uzyskać więcej informacji, przeczytaj %sInstrukcję Użytkownika AUR%s oraz %sInstrukcję Zaufanego Użytkownika%s." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -226,8 +227,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "Nie zapomnij zagłosować na swoje ulubione pakiety!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Część pakietów może być dostępna w formie binarnej w [community]." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Część pakietów może być dostępna w formie binarnej w [extra]." #: html/home.php msgid "DISCLAIMER" @@ -276,7 +277,7 @@ msgstr "Prośba o usunięcie" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -319,10 +320,11 @@ msgstr "Dyskusja" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "Ogólna dyskusja dotycząca Repozytorium Użytkowników Arch (AUR) i struktury Zaufanych Użytkowników odbywa się na %saur-general%s. Dyskusja dotycząca rozwoju interfejsu webowego AUR odbywa się zaś na liście dyskusyjnej %saur-dev%s." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -333,9 +335,9 @@ msgstr "Zgłaszanie błędów" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "Jeśli znalazłeś błąd w interfejsie webowym AUR, zgłoś go w naszym %ssystemie śledzenia błędów%s. Używaj go %swyłącznie%s do zgłaszania błędów związanych z interfejsem webowym AUR. Błędy powiązane z pakietami zgłaszaj ich opiekunom lub zostaw komentarz pod odpowiednim pakietem." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "" #: html/home.php msgid "Package Search" @@ -535,8 +537,8 @@ msgid "Delete" msgstr "Usuń" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Tylko Zaufani Użytkownicy i Deweloperzy mogą usuwać pakiety." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -576,8 +578,8 @@ msgid "Disown" msgstr "Porzuć" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "Tylko Zaufani Użytkownicy i Programiści mogą porzucać pakiety." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -666,8 +668,8 @@ msgid "Merge" msgstr "Scal" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Tylko Zaufani Użytkownicy i Deweloperzy mogą scalać pakiety." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -712,7 +714,7 @@ msgstr "Zasady korzystania" #: html/tos.php msgid "" "The following documents have been updated. Please review them carefully:" -msgstr "" +msgstr "Zaktualizowano następujące dokumenty. Przejrzyj je dokładnie:" #: html/tos.php #, php-format @@ -724,8 +726,8 @@ msgid "I accept the terms and conditions above." msgstr "Akceptuję powyższe warunki i zasady." #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Zaufany Użytkownik" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -736,8 +738,8 @@ msgid "Voting is closed for this proposal." msgstr "Głosowanie na tą propozycję jest zamknięte." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Tylko Zaufani Użytkownicy mogą głosować." +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -792,7 +794,7 @@ msgstr "Może zawierać tylko jedną kropkę, podkreślnik lub myślnik." #: lib/acctfuncs.inc.php msgid "Please confirm your new password." -msgstr "" +msgstr "Potwierdź nowe hasło." #: lib/acctfuncs.inc.php msgid "The email address is invalid." @@ -800,7 +802,7 @@ msgstr "Adres e-mail jest nieprawidłowy." #: lib/acctfuncs.inc.php msgid "The backup email address is invalid." -msgstr "" +msgstr "Zapasowy adres e-mail jest nieprawidłowy." #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." @@ -824,7 +826,7 @@ msgstr "Język nie jest obecnie obsługiwany." #: lib/acctfuncs.inc.php msgid "Timezone is not currently supported." -msgstr "" +msgstr "Strefa czasowa nie jest obecnie obsługiwana." #: lib/acctfuncs.inc.php #, php-format @@ -1232,8 +1234,8 @@ msgstr "Deweloper" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "Zaufany Użytkownik i Developer" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1335,10 +1337,6 @@ msgstr "" msgid "Normal user" msgstr "Zwykły użytkownik" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Zaufany Użytkownik" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Konto zablokowane" @@ -1411,6 +1409,15 @@ msgid "" " the Arch User Repository." msgstr "Następująca informacja jest wymagana jedynie w sytuacji, gdy chcesz przesłać pakiety do Repozytorium Użytkowników Arch." +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "Klucz publiczny SSH" @@ -1836,22 +1843,22 @@ msgstr "Scal z" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2116,8 +2123,8 @@ msgid "Registered Users" msgstr "Zarejestrowani użytkownicy" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Zaufani użytkownicy" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2302,7 +2309,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2356,3 +2363,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/pt.po b/po/pt.po index b2cf86b2..33e01d7d 100644 --- a/po/pt.po +++ b/po/pt.po @@ -7,16 +7,16 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Portuguese (http://www.transifex.com/lfleischer/aurweb/language/pt/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Lukas Fleischer , 2011\n" +"Language-Team: Portuguese (http://app.transifex.com/lfleischer/aurweb/language/pt/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: pt\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #: html/404.php msgid "Page Not Found" @@ -132,15 +132,15 @@ msgid "Type" msgstr "Tipo" #: html/addvote.php -msgid "Addition of a TU" +msgid "Addition of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU" +msgid "Removal of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" msgstr "" #: html/addvote.php @@ -198,8 +198,9 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." msgstr "" #: html/home.php @@ -214,7 +215,7 @@ msgid "Remember to vote for your favourite packages!" msgstr "Lembre-se de votar nos seus pacotes favoritos!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." +msgid "Some packages may be provided as binaries in [extra]." msgstr "" #: html/home.php @@ -264,7 +265,7 @@ msgstr "" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -307,9 +308,10 @@ msgstr "Discussão" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -321,8 +323,8 @@ msgstr "" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -523,7 +525,7 @@ msgid "Delete" msgstr "" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." +msgid "Only Package Maintainers and Developers can delete packages." msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php @@ -564,7 +566,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -654,7 +656,7 @@ msgid "Merge" msgstr "" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." +msgid "Only Package Maintainers and Developers can merge packages." msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php @@ -712,8 +714,8 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Usuário Confiável" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -724,7 +726,7 @@ msgid "Voting is closed for this proposal." msgstr "" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -1220,7 +1222,7 @@ msgstr "Desenvolvedor" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1323,10 +1325,6 @@ msgstr "" msgid "Normal user" msgstr "Usuário Normal" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Usuário Confiável" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Conta Suspensa" @@ -1399,6 +1397,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1579,6 +1586,7 @@ msgid "%d pending request" msgid_plural "%d pending requests" msgstr[0] "" msgstr[1] "" +msgstr[2] "" #: template/pkgbase_actions.php msgid "Adopt Package" @@ -1822,22 +1830,22 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -1853,6 +1861,7 @@ msgid "%d package request found." msgid_plural "%d package requests found." msgstr[0] "" msgstr[1] "" +msgstr[2] "" #: template/pkgreq_results.php template/pkg_search_results.php #, php-format @@ -1877,6 +1886,7 @@ msgid "~%d day left" msgid_plural "~%d days left" msgstr[0] "" msgstr[1] "" +msgstr[2] "" #: template/pkgreq_results.php #, php-format @@ -1884,6 +1894,7 @@ msgid "~%d hour left" msgid_plural "~%d hours left" msgstr[0] "" msgstr[1] "" +msgstr[2] "" #: template/pkgreq_results.php msgid "<1 hour left" @@ -2012,6 +2023,7 @@ msgid "%d package found." msgid_plural "%d packages found." msgstr[0] "" msgstr[1] "" +msgstr[2] "" #: template/pkg_search_results.php msgid "Version" @@ -2094,8 +2106,8 @@ msgid "Registered Users" msgstr "Usuários Registrados" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Usuários Confiáveis" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2280,7 +2292,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2334,3 +2346,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/pt_BR.po b/po/pt_BR.po index c9c15d72..c689a8ab 100644 --- a/po/pt_BR.po +++ b/po/pt_BR.po @@ -6,23 +6,23 @@ # Albino Biasutti Neto Bino , 2011 # Fábio Nogueira , 2016 # Rafael Fontenelle , 2012-2015 -# Rafael Fontenelle , 2011,2015-2018,2020-2022 +# Rafael Fontenelle , 2011,2015-2018,2020-2023 # Rafael Fontenelle , 2011 # Sandro , 2011 # Sandro , 2011 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Portuguese (Brazil) (http://www.transifex.com/lfleischer/aurweb/language/pt_BR/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Rafael Fontenelle , 2011,2015-2018,2020-2023\n" +"Language-Team: Portuguese (Brazil) (http://app.transifex.com/lfleischer/aurweb/language/pt_BR/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: pt_BR\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #: html/404.php msgid "Page Not Found" @@ -138,16 +138,16 @@ msgid "Type" msgstr "Tipo" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Adição de um TU" +msgid "Addition of a Package Maintainer" +msgstr "Adição de Mantenedor de Pacote" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Remoção de um TU" +msgid "Removal of a Package Maintainer" +msgstr "Remoção de um Mantenedor de Pacote" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Remoção de um TU (inatividade não declarada)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "Remoção de um Mantenedor de Pacote (inatividade não declarada)" #: html/addvote.php msgid "Amendment of Bylaws" @@ -204,9 +204,10 @@ msgstr "Pesquisar por pacotes que eu comantenho" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Bem-vindo ao AUR! Por favor, leia as %sDiretrizes de Usuário do AUR%s e %sDiretrizes de UC do AUR%s para mais informações." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "Bem-vindo ao AUR! Por favor, leia as %sDiretrizes do Usuário do AUR%s para obter mais informações e as %sDiretrizes de Envio para o AUR%s se você quiser contribuir com um PKGBUILD." #: html/home.php #, php-format @@ -220,8 +221,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "Lembre-se de votar nos seus pacotes favoritos!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Alguns pacotes podem ser fornecidos como binários no repositório [community]." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Alguns pacotes podem ser fornecidos como binários no repositório [extra]." #: html/home.php msgid "DISCLAIMER" @@ -270,8 +271,8 @@ msgstr "Requisição de exclusão" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "Requisite que um pacote seja removido do Arch User Repository. Por favor, não use esta opção se um pacote está quebrado, mas que pode ser corrigido facilmente. Ao invés disso, contate o mantenedor do pacote e, se necessário, preencha uma requisição para tornar o pacote órfão." +"the maintainer and file orphan request if necessary." +msgstr "Requisite que um pacote seja removido do Arch User Repository. Por favor, não use esta opção se um pacote está quebrado, mas que pode ser corrigido facilmente. Ao invés disso, contate o mantenedor e, se necessário, preencha uma requisição para tornar o pacote órfão." #: html/home.php msgid "Merge Request" @@ -288,7 +289,7 @@ msgstr "Requisite que um pacote seja mesclado com outro. Pode ser usado quando u msgid "" "If you want to discuss a request, you can use the %saur-requests%s mailing " "list. However, please do not use that list to file requests." -msgstr "Se você quiser discutir uma requisição, você pode usar a lista de discussão %saur-request%s. Porém, por favor não use essa lista para fazer requisições." +msgstr "Se você quiser discutir uma requisição, você pode usar a lista de discussão %saur-request%s. Porém, por favor, não use essa lista para fazer requisições." #: html/home.php msgid "Submitting Packages" @@ -313,10 +314,11 @@ msgstr "Discussão" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "Discussões gerais no que se refere à estrutura do Arch User Repository (AUR) e de Trusted Users acontecem no %saur-general%s. Para discussão relacionada ao desenvolvimento da interface web do AUR, use a lista de discussão do %saur-dev%s" +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "Discussões gerais no que se refere à estrutura do Arch User Repository (AUR) e de Mantenedores de Pacote acontecem no %saur-general%s. Para discussão relacionada ao desenvolvimento da interface web do AUR, use a lista de discussão do %saur-dev%s" #: html/home.php msgid "Bug Reporting" @@ -327,9 +329,9 @@ msgstr "Relatório de erros" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "Se você encontrar um erro na interface web do AUR, por favor preencha um relatório de erro no nosso %srastreador de erros%s. Use o rastreador para relatar erros encontrados no AUR web, %ssomente%s. Para relatar erros de empacotamento, contate o mantenedor do pacote ou deixe um comentário na página de pacote apropriada." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "Se você encontrar um erro na interface web do AUR, preencha um relatório de erro no nosso %srastreador de erros%s. Use o rastreador para relatar erros encontrados na interface web do AUR, %ssomente%s. Para relatar erros de empacotamento, contate o mantenedor ou deixe um comentário na página de pacote apropriada." #: html/home.php msgid "Package Search" @@ -361,12 +363,12 @@ msgstr "Desmarcar" #: html/login.php template/header.php msgid "Login" -msgstr "Conectar" +msgstr "Login" #: html/login.php html/tos.php #, php-format msgid "Logged-in as: %s" -msgstr "Conectado como: %s" +msgstr "Logado como: %s" #: html/login.php template/header.php msgid "Logout" @@ -374,7 +376,7 @@ msgstr "Sair" #: html/login.php msgid "Enter login credentials" -msgstr "Digite as credenciais para se conectar" +msgstr "Digite as credenciais para se logar" #: html/login.php msgid "User name or primary email address" @@ -396,7 +398,7 @@ msgstr "Esqueci minha senha" #, php-format msgid "" "HTTP login is disabled. Please %sswitch to HTTPs%s if you want to login." -msgstr "Acesso por HTTP está desabilitado. Favor %sacesse via HTTPs%s caso queira se conectar." +msgstr "Acesso por HTTP está desabilitado. %sAcesse via HTTPs%s caso queira se conectar." #: html/packages.php template/pkg_search_form.php msgid "Search Criteria" @@ -434,7 +436,7 @@ msgstr "Redefinição de senha" #: html/passreset.php msgid "Check your e-mail for the confirmation link." -msgstr "Verifique no seu e-mail pelo link de confirmação." +msgstr "Confira seu e-mail pelo link de confirmação." #: html/passreset.php msgid "Your password has been reset successfully." @@ -446,7 +448,7 @@ msgstr "Confirme seu nome de usuário ou endereço de e-mail primário:" #: html/passreset.php msgid "Enter your new password:" -msgstr "Digite a sua senha:" +msgstr "Digite a sua nova senha:" #: html/passreset.php msgid "Confirm your new password:" @@ -529,8 +531,8 @@ msgid "Delete" msgstr "Excluir" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Somente Trusted Users e Desenvolvedores podem excluir pacotes." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "Apenas Mantenedores de Pacote e Desenvolvedores podem excluir pacotes." #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -570,8 +572,8 @@ msgid "Disown" msgstr "Abandonar" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "Apenas Trusted Users e Desenvolvedores podem abandonar pacotes." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "Apenas Mantenedores de Pacote e Desenvolvedores podem tornar órfãos pacotes." #: html/pkgflagcomment.php msgid "Flag Comment" @@ -660,8 +662,8 @@ msgid "Merge" msgstr "Mesclar" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Somente Trusted Users e Desenvolvedores podem mesclar pacotes." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "Apenas Mantenedores de Pacote e Desenvolvedores podem mesclar pacotes." #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -718,8 +720,8 @@ msgid "I accept the terms and conditions above." msgstr "Eu aceito os termos e condições acima." #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Trusted User" +msgid "Package Maintainer" +msgstr "Mantenedor de Pacote" #: html/tu.php msgid "Could not retrieve proposal details." @@ -730,8 +732,8 @@ msgid "Voting is closed for this proposal." msgstr "A votação está encerrada para esta proposta." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Apenas Trusted Users têm permissão para votar." +msgid "Only Package Maintainers are allowed to vote." +msgstr "Apenas Mantenedores de Pacote podem votar." #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -1226,8 +1228,8 @@ msgstr "Desenvolvedor" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "Trusted User & Desenvolvedor" +msgid "Package Maintainer & Developer" +msgstr "Mantenedor de Pacote & Desenvolvedor" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1329,10 +1331,6 @@ msgstr "Seu nome de usuário é o nome que você vai usar para se autenticar. É msgid "Normal user" msgstr "Usuário normal" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Trusted user" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Conta suspensa" @@ -1405,6 +1403,15 @@ msgid "" " the Arch User Repository." msgstr "A informação a seguir é necessária apenas se você deseja enviar pacotes para o Arch User Repository." +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "Especifique várias chaves SSH separadas por nova linha, linhas vazias são ignoradas." + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "Ocultar comentários excluídos" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "Chave pública SSH" @@ -1585,6 +1592,7 @@ msgid "%d pending request" msgid_plural "%d pending requests" msgstr[0] "%d requisição pendente" msgstr[1] "%d requisições pendentes" +msgstr[2] "%d requisições pendentes" #: template/pkgbase_actions.php msgid "Adopt Package" @@ -1828,26 +1836,26 @@ msgstr "Mesclar em" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "Ao enviar uma requisição de exclusão, você solicita que um Trusted User exclua o pacote base. Esse tipo de requisição deveria ser usada em caso de duplicidade, softwares abandonados pelo upstream, assim como pacotes ilegais ou irreparavelmente quebrados." +msgstr "Ao enviar uma requisição de exclusão, você solicita que um Mantenedor de Pacote exclua o pacote base. Esse tipo de requisição deveria ser usada em caso de duplicidade, softwares abandonados pelo upstream, assim como pacotes ilegais ou irreparavelmente quebrados." #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "Ao enviar uma requisição de mesclagem, você solicita que um Trusted User exclua o pacote base e transfira seus votos e comentários para um outro pacote base. Mesclar um pacote não afeta os repositórios Git correspondentes. Certifique-se de você mesmo atualizar o histórico Git do pacote alvo." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "Ao enviar uma requisição de mesclagem, você solicita que um Mantenedor de Pacote exclua o pacote base e transfira seus votos e comentários para um outro pacote base. Mesclar um pacote não afeta os repositórios Git correspondentes. Certifique-se de você mesmo atualizar o histórico Git do pacote alvo." #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "Ao enviar uma requisição de tornar órfão, você pede que um Trusted User abandona o pacote base. Por favor, apenas faça isto se o pacote precise de ação do mantenedor, estando este ausente por muito tempo, e você já tentou – e não conseguiu – contatá-lo anteriormente." +msgstr "Ao enviar uma requisição de tornar órfão, você pede que um Mantenedor de Pacote abandona o pacote base. Por favor, apenas faça isto se o pacote precise de ação do mantenedor, estando este ausente por muito tempo, e você já tentou – e não conseguiu – contatá-lo anteriormente." #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -1859,6 +1867,7 @@ msgid "%d package request found." msgid_plural "%d package requests found." msgstr[0] "%d requisição de pacote encontrada." msgstr[1] "%d requisições de pacotes encontradas." +msgstr[2] "%d requisições de pacotes encontradas." #: template/pkgreq_results.php template/pkg_search_results.php #, php-format @@ -1883,6 +1892,7 @@ msgid "~%d day left" msgid_plural "~%d days left" msgstr[0] "~%d dia restante" msgstr[1] "~%d dias restantes" +msgstr[2] "~%d dias restantes" #: template/pkgreq_results.php #, php-format @@ -1890,6 +1900,7 @@ msgid "~%d hour left" msgid_plural "~%d hours left" msgstr[0] "~%d hora restante" msgstr[1] "~%d horas restantes" +msgstr[2] "~%d horas restantes" #: template/pkgreq_results.php msgid "<1 hour left" @@ -2018,6 +2029,7 @@ msgid "%d package found." msgid_plural "%d packages found." msgstr[0] "%d pacote encontrado." msgstr[1] "%d pacotes encontrados." +msgstr[2] "%d pacotes encontrados." #: template/pkg_search_results.php msgid "Version" @@ -2100,8 +2112,8 @@ msgid "Registered Users" msgstr "Usuários registrados" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Trusted Users" +msgid "Package Maintainers" +msgstr "Mantenedores de Pacote" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2286,8 +2298,8 @@ msgstr "{user} [1] excluiu {pkgbase} [2].\n\nVocê não mais receberá notifica #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" -msgstr "Lembrete de votação de TU: Proposta {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" +msgstr "Lembrete de Voto de Mantenedor de Pacote: Proposta {id}" #: scripts/notify.py #, python-brace-format @@ -2333,10 +2345,42 @@ msgstr "Erro do Servidor AUR" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "Comentários relacionados ao fechamento de requisição de pacote..." #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." -msgstr "" +msgstr "Esta ação fechará todas as requisições de pacote pendentes relacionadas a ela. Se %sComentários%s forem omitidos, um comentário de encerramento será gerado automaticamente." + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "atribuído" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "Mostrar %d mais" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "dependências" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "A conta não foi excluída, marque a caixa de confirmação." + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "Cancelar" + +#: templates/requests.html +msgid "Package name" +msgstr "Nome do pacote" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "Observe que se você ocultar seu endereço de e-mail, ele será incluído na lista de CCO para quaisquer notificações de requisição. Caso alguém responda a essas notificações, você não receberá um e-mail. No entanto, as respostas geralmente são enviadas para a lista de discussão e, portanto, seriam visíveis no arquivo." diff --git a/po/pt_PT.po b/po/pt_PT.po index 3518cb7b..8d065e15 100644 --- a/po/pt_PT.po +++ b/po/pt_PT.po @@ -12,16 +12,16 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Portuguese (Portugal) (http://www.transifex.com/lfleischer/aurweb/language/pt_PT/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Christophe Silva , 2018\n" +"Language-Team: Portuguese (Portugal) (http://app.transifex.com/lfleischer/aurweb/language/pt_PT/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Language: pt_PT\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Plural-Forms: nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2;\n" #: html/404.php msgid "Page Not Found" @@ -137,16 +137,16 @@ msgid "Type" msgstr "Tipo" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Adição de um TU" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Remoção de um TU" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Remoção de um TU (inatividade não declarada)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -203,9 +203,10 @@ msgstr "Procurar por pacotes que eu co-mantenho" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Bem-vindo ao AUR! Por favor leia as %sOrientações de Utilizador do AUR%s e %sOrientações de TU do AUR%s para mais informações." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -219,8 +220,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "Lembre-se de votar nos seus pacotes favoritos!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Alguns dos pacotes podem ser fornecidos como binários no repositório [community]." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Alguns dos pacotes podem ser fornecidos como binários no repositório [extra]." #: html/home.php msgid "DISCLAIMER" @@ -269,8 +270,8 @@ msgstr "Pedido para Apagar" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "Pedido para que um pacote seja removido do Arch User Repository. Por favor não use este pedido se um pacote está danificado e pode ser resolvido facilmente. Contacte o responsável pelo mesmo e faça um Pedido para Tornar Orfão se necessário." +"the maintainer and file orphan request if necessary." +msgstr "" #: html/home.php msgid "Merge Request" @@ -312,10 +313,11 @@ msgstr "Discussão" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "A discussão geral sobre o Repositório do Usuário do Arco (AUR) e a estrutura do Usuário Confiável ocorre em %s aur-general %s. Para discussão relacionada ao desenvolvimento da interface da web AUR, use a lista de emails %s aur-dev %s" +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -326,8 +328,8 @@ msgstr "Reportar um Bug" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -528,8 +530,8 @@ msgid "Delete" msgstr "Eliminar" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Apenas Utilizadores de Confiança e Programadores podem eliminar pacotes." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -569,8 +571,8 @@ msgid "Disown" msgstr "Renunciar" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "Somente Usuários Verificados e Desenvolvedores podem desaprovar pacotes." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -659,8 +661,8 @@ msgid "Merge" msgstr "Fundir" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Apenas Utilizadores de Confiança e Programadores podem fundir pacotes." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -717,8 +719,8 @@ msgid "I accept the terms and conditions above." msgstr "Eu aceito os termos e condições acima mencionados." #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Utilizador de Confiança" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -729,8 +731,8 @@ msgid "Voting is closed for this proposal." msgstr "A votação está fechada para esta proposta." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Apenas Utilizadores de Confiança podem votar." +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -1225,7 +1227,7 @@ msgstr "Desenvolvedor" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1328,10 +1330,6 @@ msgstr "" msgid "Normal user" msgstr "Utilizador normal" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Utilizador de confiança" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Conta Suspensa" @@ -1404,6 +1402,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1584,6 +1591,7 @@ msgid "%d pending request" msgid_plural "%d pending requests" msgstr[0] "%d pedido por atender" msgstr[1] "%d pedidos por atender" +msgstr[2] "%d pedidos por atender" #: template/pkgbase_actions.php msgid "Adopt Package" @@ -1827,22 +1835,22 @@ msgstr "Juntar em" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -1858,6 +1866,7 @@ msgid "%d package request found." msgid_plural "%d package requests found." msgstr[0] "%d pedido de pacote encontrado." msgstr[1] "%d pedidos de pacotes encontrados." +msgstr[2] "%d pedidos de pacotes encontrados." #: template/pkgreq_results.php template/pkg_search_results.php #, php-format @@ -1882,6 +1891,7 @@ msgid "~%d day left" msgid_plural "~%d days left" msgstr[0] "" msgstr[1] "" +msgstr[2] "" #: template/pkgreq_results.php #, php-format @@ -1889,6 +1899,7 @@ msgid "~%d hour left" msgid_plural "~%d hours left" msgstr[0] "" msgstr[1] "" +msgstr[2] "" #: template/pkgreq_results.php msgid "<1 hour left" @@ -2017,6 +2028,7 @@ msgid "%d package found." msgid_plural "%d packages found." msgstr[0] "%d pacote encontrado." msgstr[1] "%d pacotes encontrados." +msgstr[2] "%d pacotes encontrados." #: template/pkg_search_results.php msgid "Version" @@ -2099,8 +2111,8 @@ msgid "Registered Users" msgstr "Utilizadores Registados" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Utilizadores de Confiança" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2285,7 +2297,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2339,3 +2351,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/ro.po b/po/ro.po index fa159928..0a58d4fc 100644 --- a/po/ro.po +++ b/po/ro.po @@ -5,15 +5,16 @@ # Translators: # Arthur Țițeică , 2013-2015 # Lukas Fleischer , 2011 +# Marius Tcaci, 2023 # Mihai Coman , 2011-2014 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Romanian (http://www.transifex.com/lfleischer/aurweb/language/ro/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Marius Tcaci, 2023\n" +"Language-Team: Romanian (http://app.transifex.com/lfleischer/aurweb/language/ro/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -48,7 +49,7 @@ msgstr "" #: html/503.php msgid "Service Unavailable" -msgstr "" +msgstr "Serviciu indisponibil" #: html/503.php msgid "" @@ -77,7 +78,7 @@ msgstr "Nu ai permisiune pentru a modifica acest cont." #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." -msgstr "" +msgstr "Parolă invalidă" #: html/account.php msgid "Use this form to search existing accounts." @@ -134,16 +135,16 @@ msgid "Type" msgstr "Tip" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Adăugarea unui TU" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Înlăturarea unui TU" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Înlăturarea unui TU (inactivitate nedeclarată)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -163,7 +164,7 @@ msgstr "" #: html/commentedit.php template/pkg_comments.php msgid "Edit comment" -msgstr "" +msgstr "Editează comentariul" #: html/home.php template/header.php msgid "Dashboard" @@ -179,7 +180,7 @@ msgstr "" #: html/home.php msgid "My Requests" -msgstr "" +msgstr "Cererile mele" #: html/home.php msgid "My Packages" @@ -187,7 +188,7 @@ msgstr "Pachetele mele" #: html/home.php msgid "Search for packages I maintain" -msgstr "" +msgstr "Caută pachetele pe care le intretin" #: html/home.php msgid "Co-Maintained Packages" @@ -200,9 +201,10 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Bine ai venit la AUR! Te rog citește %sGhidul utilizatorului AUR%s și %sGhidul AUR TU%s pentru mai multe informații." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -216,8 +218,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "Nu uita să votezi pentru pachetele tale favorite!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Unele pachete pot fi furnizate ca binare în [community]." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Unele pachete pot fi furnizate ca binare în [extra]." #: html/home.php msgid "DISCLAIMER" @@ -227,11 +229,11 @@ msgstr "DECLARAȚIE DE NEASUMARE A RESPONSABILITĂȚII" msgid "" "AUR packages are user produced content. Any use of the provided files is at " "your own risk." -msgstr "" +msgstr "Pachetele AUR sunt continut produs de utilizatori. Orice utilizare a fisierelor puse la dispozitie se face pe propriul risc." #: html/home.php msgid "Learn more..." -msgstr "" +msgstr "Descoperă mai multe..." #: html/home.php msgid "Support" @@ -266,7 +268,7 @@ msgstr "" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -309,9 +311,10 @@ msgstr "Discuție" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -323,8 +326,8 @@ msgstr "Semnalare buguri" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -525,8 +528,8 @@ msgid "Delete" msgstr "Șterge" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Numai Dezvoltatorii și Trusted Users pot șterge pachete." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -566,7 +569,7 @@ msgid "Disown" msgstr "Abandonează" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -656,8 +659,8 @@ msgid "Merge" msgstr "Fuzionare" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Numai Dezvoltatorii și Trusted Users pot fuziona pachete." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -714,8 +717,8 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Trusted User" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -726,8 +729,8 @@ msgid "Voting is closed for this proposal." msgstr "Votarea este închisă pentru această propunere." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Doar Trusted Users au permisiunea să voteze." +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -1222,8 +1225,8 @@ msgstr "Dezvoltator" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "Utilizator de încredere (TU) & Dezvoltator" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1325,10 +1328,6 @@ msgstr "" msgid "Normal user" msgstr "Utilizator obișnuit" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Trusted user" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Cont suspendat" @@ -1401,6 +1400,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1630,7 +1638,7 @@ msgstr "Voturi" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php template/pkg_search_results.php msgid "Popularity" -msgstr "" +msgstr "Popularitate" #: template/pkgbase_details.php template/pkg_details.php msgid "First Submitted" @@ -1825,22 +1833,22 @@ msgstr "Fuzionează" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -1962,7 +1970,7 @@ msgstr "Votat" #: template/pkg_search_form.php msgid "Last modified" -msgstr "" +msgstr "Ultima modificare" #: template/pkg_search_form.php msgid "Ascending" @@ -2101,8 +2109,8 @@ msgid "Registered Users" msgstr "Utilizatori înregistrați" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Trusted users" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2287,7 +2295,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2341,3 +2349,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/ru.po b/po/ru.po index 75550c8c..ca2890a2 100644 --- a/po/ru.po +++ b/po/ru.po @@ -18,11 +18,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Russian (http://www.transifex.com/lfleischer/aurweb/language/ru/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Kevin Morris , 2021\n" +"Language-Team: Russian (http://app.transifex.com/lfleischer/aurweb/language/ru/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -143,16 +143,16 @@ msgid "Type" msgstr "Тип" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Добавление TU" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Удаление TU" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Удаление TU (неактивность без уведомлений)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -209,9 +209,10 @@ msgstr "Поиск пакетов, в которых я сопровождающ #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Добро пожаловать в AUR! Пожалуйста, ознакомьтесь с %sРуководством пользователя AUR%s и с %sРуководством доверенного пользователя AUR%s, чтобы узнать больше." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -225,8 +226,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "Не забывайте голосовать за понравившиеся вам пакеты!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "В хранилище [community] некоторые пакеты могут быть представлены в бинарном виде." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "В хранилище [extra] некоторые пакеты могут быть представлены в бинарном виде." #: html/home.php msgid "DISCLAIMER" @@ -275,8 +276,8 @@ msgstr "Запрос на удаление" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "Запросить удаление пакета из AUR. Пожалуйста, не используйте это действие, если пакет не собирается и это может быть легко исправлено. Вместо этого, свяжитесь с сопровождающим и отправьте запрос на смену сопровождающего, если необходимо." +"the maintainer and file orphan request if necessary." +msgstr "" #: html/home.php msgid "Merge Request" @@ -318,10 +319,11 @@ msgstr "Обсуждение" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "Общее обсуждение Пользовательского Репозитория ArchLinux (AUR) и структуры Доверенных Пользователей ведется в %saur-general%s. Для обсуждение разработки веб-интерфейса AUR используйте %saur-dev%s." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -332,9 +334,9 @@ msgstr "Отчет об ошибке" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "Если вы нашли баг в интерфейсе AUR, пожалуйста, отправьте сообщение на %sбаг трекер%s. Используйте данный баг трекер %sтолько%s для сообщениях о багах в AUR. Если вы хотите сообщить о баге в пакете, свяжитесь с сопровождающим, или оставьте комментарий на соответствующей странице." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "" #: html/home.php msgid "Package Search" @@ -534,8 +536,8 @@ msgid "Delete" msgstr "Удалить" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Только Доверенные Пользователи и Разработчики могут удалять пакеты." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -575,8 +577,8 @@ msgid "Disown" msgstr "Бросить" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "Только Доверенные Пользователи или разработчики могут бросить пакеты." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -665,8 +667,8 @@ msgid "Merge" msgstr "Объединить" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Только Доверенные Пользователи и Разработчики могут объединять пакеты." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -723,8 +725,8 @@ msgid "I accept the terms and conditions above." msgstr "Я принимаю приведённые выше положения и условия." #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Доверенный пользователь" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -735,8 +737,8 @@ msgid "Voting is closed for this proposal." msgstr "Голосование закрыто." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Только Доверенные Пользователи имеют право голоса." +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -1231,8 +1233,8 @@ msgstr "Разработчик" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "Доверенные пользователи и Разработчики" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1334,10 +1336,6 @@ msgstr "Имя пользователя имя, которое будет ис msgid "Normal user" msgstr "Обычный пользователь" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Доверенный пользователь" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Действие учетной записи приостановлено" @@ -1410,6 +1408,15 @@ msgid "" " the Arch User Repository." msgstr "Следующая информация необходима, только если вы хотите загружать пакеты в AUR." +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "Публичный SSH ключ" @@ -1835,26 +1842,26 @@ msgstr "Объединить с" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "Отправляя запрос удаления, Вы просите Доверенного Пользователя удалить основной пакет. Этот тип запроса должен использоваться для дубликатов, заброшенных, а также незаконных и безнадёжно сломанных пакетов." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "Отправляя запрос на слияние, Вы просите Доверенного Пользователя удалить основной пакет, переместить голоса и комментарии к другому основному пакету. Слияние пакета не влияет на соответствующие Git репозитории. Обновите Git историю целевого пакета самостоятельно." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "Отправляя сиротский запрос, Вы просите Доверенного Пользователя бросить основной пакет. Пожалуйста, делайте это, только если пакет нуждается в действиях обслуживающего, который недоступен и вы уже пытались связаться с ним ранее." +msgstr "" #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -2115,8 +2122,8 @@ msgid "Registered Users" msgstr "Зарегистрированных пользователей" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Доверенных пользователей" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2301,7 +2308,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2355,3 +2362,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/sk.po b/po/sk.po index 76d3d1a8..a54f145f 100644 --- a/po/sk.po +++ b/po/sk.po @@ -4,16 +4,16 @@ # # Translators: # archetyp , 2013-2016 -# Jose Riha , 2018 +# Jose Riha , 2018,2022 # Matej Ľach , 2011 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Slovak (http://www.transifex.com/lfleischer/aurweb/language/sk/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Jose Riha , 2018,2022\n" +"Language-Team: Slovak (http://app.transifex.com/lfleischer/aurweb/language/sk/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -77,7 +77,7 @@ msgstr "Nemáte potrebné oprávnenia, pre úpravu tohoto účtu. " #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." -msgstr "" +msgstr "Neplatné heslo." #: html/account.php msgid "Use this form to search existing accounts." @@ -134,16 +134,16 @@ msgid "Type" msgstr "Typ" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Pridanie TU" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Odobranie TU" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Odobranie TU (neohlásená neaktivita)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -167,7 +167,7 @@ msgstr "Editovať komentár" #: html/home.php template/header.php msgid "Dashboard" -msgstr "" +msgstr "Nástenka" #: html/home.php template/header.php msgid "Home" @@ -175,11 +175,11 @@ msgstr "Domov" #: html/home.php msgid "My Flagged Packages" -msgstr "" +msgstr "Moje označené balíčky" #: html/home.php msgid "My Requests" -msgstr "" +msgstr "Moje požiadavky" #: html/home.php msgid "My Packages" @@ -187,22 +187,23 @@ msgstr "Moje balíčky" #: html/home.php msgid "Search for packages I maintain" -msgstr "" +msgstr "Hľadať balíčky, ktoré spravujem" #: html/home.php msgid "Co-Maintained Packages" -msgstr "" +msgstr "Spoločne spravované balíčky" #: html/home.php msgid "Search for packages I co-maintain" -msgstr "" +msgstr "Hľadať balíčky, v ktorých pôsobím ako spolupracovník" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Vitajte v AUR! Prečítajte si prosím %sAUR smernicu pre užívateľov%s a %sAUR smernicu pre TU%s pre ďalšie informácie." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -216,8 +217,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "Nezabudnite hlasovať za svoje obľúbené balíčky!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Niektoré balíčky môžu byť poskytnuté ako binárky v [community]. " +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Niektoré balíčky môžu byť poskytnuté ako binárky v [extra]. " #: html/home.php msgid "DISCLAIMER" @@ -239,7 +240,7 @@ msgstr "Podpora" #: html/home.php msgid "Package Requests" -msgstr "Žiadosti ohľadom balíčkov" +msgstr "Žiadosti týkajúce sa balíčkov" #: html/home.php #, php-format @@ -266,8 +267,8 @@ msgstr "Žiadosť o vymazanie" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "Žiadosť o odstránenie balíčka z AUR. Nepoužívajte prosím túto možnosť v prípade, že balíček je pokazený a možno ho ľahko opraviť. Namiesto toho radšej kontaktujte jeho správcu alebo v prípade nutnosti požiadajte o jeho osirenie." +"the maintainer and file orphan request if necessary." +msgstr "" #: html/home.php msgid "Merge Request" @@ -309,10 +310,11 @@ msgstr "Diskusia" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "Všeobecná diskusia týkajúca sa Arch Užívateľského Repozitára (AUR) a štruktúry dôverovaných užívateľov (TU) je na %saur-general%s. Na diskusiu týkajúcu sa vývoja AUR webu použite %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -323,9 +325,9 @@ msgstr "Ohlasovanie chýb" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "Ak nájdete chybu vo webovom rozhradní AUR, pošlite prosím správu o chybe na náš %sbug tracker%s. Posielajte sem %slen%s chyby webového rozhrania AUR. Pre nahlásenie chýb balíčkov kontaktujte správcu balíčka alebo zanechate komentár na príslušnej stránke balíčka." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "" #: html/home.php msgid "Package Search" @@ -374,7 +376,7 @@ msgstr "Zadajte prihlasovacie údaje" #: html/login.php msgid "User name or primary email address" -msgstr "" +msgstr "Meno používateľa alebo primárna e-mailová adresa" #: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" @@ -438,7 +440,7 @@ msgstr "Heslo bolo úspešne obnovené." #: html/passreset.php msgid "Confirm your user name or primary e-mail address:" -msgstr "" +msgstr "Potvrďte vaše meno používateľa alebo primárnu e-mailovú adresu:" #: html/passreset.php msgid "Enter your new password:" @@ -525,8 +527,8 @@ msgid "Delete" msgstr "Vymazať" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Len dôverovaní užívatelia a vývojári môžu vymazať balíčky." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -566,8 +568,8 @@ msgid "Disown" msgstr "Vyvlastniť" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "Len dôverovaní užívatelia a vývojári môžu vyvlastňovať balíčky." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -656,8 +658,8 @@ msgid "Merge" msgstr "Zlúčiť" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Len dôverovaní užívatelia a vývojári môžu zlúčiť balíčky." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -707,15 +709,15 @@ msgstr "" #: html/tos.php #, php-format msgid "revision %d" -msgstr "" +msgstr "revízia: %d" #: html/tos.php msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Dôverovaný užívateľ (TU)" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -726,8 +728,8 @@ msgid "Voting is closed for this proposal." msgstr "Hlasovanie o tomto návrhu bolo ukončené." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Práve hlasovať majú len dôverovaní užívatelia" +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -790,7 +792,7 @@ msgstr "E-mailová adresa nie je platná." #: lib/acctfuncs.inc.php msgid "The backup email address is invalid." -msgstr "" +msgstr "Záložná e-mailová adresa nie je platná." #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." @@ -1222,8 +1224,8 @@ msgstr "Vývojár" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "Dôverovaný užívateľ & Vývojár" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1256,7 +1258,7 @@ msgstr "PGP otlačok kľúča" #: template/account_details.php template/account_search_results.php #: template/pkgreq_results.php msgid "Status" -msgstr "Status" +msgstr "Stav" #: template/account_details.php msgid "Inactive since" @@ -1325,10 +1327,6 @@ msgstr "" msgid "Normal user" msgstr "Normálny užívateľ" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Dôverovaný užívateľ (TU)" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Účet bol pozastavený" @@ -1401,6 +1399,15 @@ msgid "" " the Arch User Repository." msgstr "Nasledujúca informácia je dôležitá iba v prípade, že chcete podať balíčky do AUR." +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "Verejný SSH kľúč" @@ -1520,7 +1527,7 @@ msgstr "Copyright %s 2004-%d aurweb Development Team." #: template/header.php msgid " My Account" -msgstr "Môj účet" +msgstr " Môj účet" #: template/pkgbase_actions.php msgid "Package Actions" @@ -1814,7 +1821,7 @@ msgstr "Typ žiadosti" #: template/pkgreq_form.php msgid "Deletion" -msgstr "Vymazanie" +msgstr "Vymazať" #: template/pkgreq_form.php msgid "Orphan" @@ -1826,26 +1833,26 @@ msgstr "Zlúčiť do" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "Odoslaním žiadosti na vymazanie balíčka žiadate dôverovaného užívateľa t.j. Trusted User, aby vymazal balíček aj s jeho základňou. Tento typ požiadavky by mal použitý pre duplikáty, softvér ku ktorému už nie sú zdroje ako aj nelegálne a neopraviteľné balíčky." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "Odoslaním žiadosti na zlúčenie balíčka žiadate dôverovaného užívateľa t.j. Trusted User, aby vymazal balíček aj s jeho základňou a preniesol hlasy a komentáre na inú základňu balíčka. Zlúčenie balíčka nezasiahne prináležiace Git repozitáre, preto sa uistite, že Git história cieľového balíčka je aktuálna." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "Odoslaním žiadosti na osirotenie balíčka žiadate dôverovaného užívateľa t.j. Trusted User, aby vyvlastnil balíček aj s jeho základňou. Toto urobte len vtedy, ak balíček vyžaduje zásah od vlastníka, vlastník nie je zastihnuteľný a už ste sa niekoľkokrát pokúšali vlastníka kontaktovať." +msgstr "" #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -1855,10 +1862,10 @@ msgstr "" #, php-format msgid "%d package request found." msgid_plural "%d package requests found." -msgstr[0] "Bola nájdená %d požiadavka ohľadom balíčkov." -msgstr[1] "Boli nájdené %d požiadavky ohľadom balíčkov." -msgstr[2] "Bolo nájdených %d požiadaviek ohľadom balíčkov." -msgstr[3] "Bolo nájdených %d požiadaviek ohľadom balíčkov." +msgstr[0] "Bola nájdená %d požiadavka týkajúc sa balíčka." +msgstr[1] "Boli nájdené %d požiadavky týkajúcich sa balíčkov." +msgstr[2] "Bolo nájdených %d požiadaviek týkajúcich sa balíčkov." +msgstr[3] "Bolo nájdených %d požiadaviek týkajúcich sa balíčkov." #: template/pkgreq_results.php template/pkg_search_results.php #, php-format @@ -1986,7 +1993,7 @@ msgstr "Vyhľadávať podľa" #: template/pkg_search_form.php template/stats/user_table.php msgid "Out of Date" -msgstr "Neaktuálny" +msgstr "Neaktuálne" #: template/pkg_search_form.php template/search_accounts_form.php msgid "Sort by" @@ -2106,8 +2113,8 @@ msgid "Registered Users" msgstr "Registrovaní užívatelia" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Dôverovaní užívatelia (TU)" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2216,7 +2223,7 @@ msgstr "" msgid "" "If you no longer wish to receive notifications about this package, please go" " to the package page [2] and select \"{label}\"." -msgstr "" +msgstr "Ak si už viac neželáte dostávať upozornenia na tento balíček, prejdite prosím na stránku balíčku [2] a vyberte \"{label}\"." #: scripts/notify.py #, python-brace-format @@ -2280,7 +2287,7 @@ msgid "" "\n" "-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." -msgstr "" +msgstr "{user} [1] zlúčil(a) {old} [2] do {new} [3].\n\n-- \nAk si už viac neželáte dostávať upozornenia na tento balíček, prejdite prosím na stránku balíčku [2] a vyberte \"{label}\"." #: scripts/notify.py #, python-brace-format @@ -2288,11 +2295,11 @@ msgid "" "{user} [1] deleted {pkgbase} [2].\n" "\n" "You will no longer receive notifications about this package." -msgstr "" +msgstr "{user} [1] odstránil(a) {pkgbase} [2].\n\nUpozornenia na tento balíček už viac nebudete dostávať." #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2339,10 +2346,42 @@ msgstr "" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "Súvisiace komentáre k žiadosti o uzatvorenie balíčka..." #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." +msgstr "Táto operácia uzavrie všetky súvisiace nevybavené žiadosti balíčkov. Ak %sComments%s vynecháte, použije sa automaticky generovaný komentár." + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." msgstr "" diff --git a/po/sr.po b/po/sr.po index dae37bcd..becffa33 100644 --- a/po/sr.po +++ b/po/sr.po @@ -9,11 +9,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Serbian (http://www.transifex.com/lfleischer/aurweb/language/sr/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Slobodan Terzić , 2011-2012,2015-2017\n" +"Language-Team: Serbian (http://app.transifex.com/lfleischer/aurweb/language/sr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -134,16 +134,16 @@ msgid "Type" msgstr "Vrsta" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Dodavanje TU" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Uklanjanje TU" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Uklanjanje TU (nedeklarisana neaktivnost)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -200,9 +200,10 @@ msgstr "Pretraži pakete koje koodržavam" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Dobrodošli u AUR! Molimo pročitajte %sSmernice za korisnike AUR-a%s i %sSmernice za poverljive korinike%s za više informacija." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -216,8 +217,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "Ne zaboravite da glasate za omiljene pakete!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Neki paketi se dostavljaju u binarnom obliku u riznici [community]." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Neki paketi se dostavljaju u binarnom obliku u riznici [extra]." #: html/home.php msgid "DISCLAIMER" @@ -266,8 +267,8 @@ msgstr "Zahtevi za brisanje" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "zahtev za brisanje paketa iz Arčove Korisničke Riznice. Ne koristite ovo ako je paket polomljen i može se lako ispraviti. Umesto toga, kontaktirajte održavaoca paketa i podnesite zahtev za odricanje ukoliko je potreban." +"the maintainer and file orphan request if necessary." +msgstr "" #: html/home.php msgid "Merge Request" @@ -309,10 +310,11 @@ msgstr "Diskusija" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "Opšta diskusija vezana za Arčovu Korisničku Riznicu (AUR) i strukturu Poverljivih korisnika se vodi na dopisnoj listi %saur-general%s.Za diskusiju o razvoju samog web sučelja AUR-a, pogedajte dopisnu listu %saur-dev%s." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -323,9 +325,9 @@ msgstr "Prijavljivanje grešaka" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "Ukoliko nađete grešku u web sučelju AUR-a. molimo da je prijavite na našem %sbubolovcu%s. Bubolovac koristite %sisključivo%s za prjavljivanje grešaka u samom AUR-u. Za prijavu grešaka u samim paketima kontaktirajte održavaoce paketa ili ostavite komentar na odgovarajućoj stranici paketa." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "" #: html/home.php msgid "Package Search" @@ -525,8 +527,8 @@ msgid "Delete" msgstr "Obriši" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Samo Poverljivi korisnici i Programeri mogu brisati pakete." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -566,8 +568,8 @@ msgid "Disown" msgstr "Odrekni se" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "Samo Poverljivi korisnici i Programeri mogu vršiti odricanje paketa." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -656,8 +658,8 @@ msgid "Merge" msgstr "Spoji" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Samo Poverljivi korisnici i Programeri mogu da spajaju pakete." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -714,8 +716,8 @@ msgid "I accept the terms and conditions above." msgstr "Prihvatam gore navedene uslove." #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Poverljivi korisnik" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -726,8 +728,8 @@ msgid "Voting is closed for this proposal." msgstr "Glasanje o ovom predlogu je završeno." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Samo poverljivi korisnici mogu da glasaju." +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -1222,8 +1224,8 @@ msgstr "Programer" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "Poverljivi korisnik i programer" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1325,10 +1327,6 @@ msgstr "Vaše korisničko ime kojim se prijavljujete. Vidljivo je u javnosti, č msgid "Normal user" msgstr "Običan korisnik" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Poverljivi korisnik" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Nalog je suspendovan" @@ -1401,6 +1399,15 @@ msgid "" " the Arch User Repository." msgstr "Sledeće informacije su neophodne ako želite da prilažete pakete u Arčovu korisničku riznicu." +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "Javni SSH ključ" @@ -1825,26 +1832,26 @@ msgstr "Stopi sa" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "Podnošenjem zahteva za brisanje tražili ste of poverljivog korisnika da obriše bazu paketa. Ovaj tip zahteva treba koristiti za duplikate, uzvodno napušten softver, kao i nelegalne ili nepopravljivo pokvarene pakete." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "Podnošenjem zahteva za spajanje tražili ste of poverljivog korisnika da obriše bazu paketa i spoji njene glasove sa drugom bazom paketa. Spajanje paketa ne utiče na pripadajuće Git riznice. Postarajte se sami da ažurirate Git istorijat ciljanog paketa." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "Podnošenjem zahteva za odricanje tražili ste od poverljivog korisnika da izvrži odricanje od baze paketa. Molimo da ovo tražite samo ukoliko paket zahteva održavanje, a održavalac je nedosupan i već ste pokušali da ga kontaktirate." +msgstr "" #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -2101,8 +2108,8 @@ msgid "Registered Users" msgstr "Registrovanih korisnika" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Poverljivih korisnika" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2287,7 +2294,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2341,3 +2348,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/sr_RS.po b/po/sr_RS.po index 985ee007..cfe5b5d3 100644 --- a/po/sr_RS.po +++ b/po/sr_RS.po @@ -7,11 +7,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Serbian (Serbia) (http://www.transifex.com/lfleischer/aurweb/language/sr_RS/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Nikola Stojković , 2013\n" +"Language-Team: Serbian (Serbia) (http://app.transifex.com/lfleischer/aurweb/language/sr_RS/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -132,15 +132,15 @@ msgid "Type" msgstr "" #: html/addvote.php -msgid "Addition of a TU" +msgid "Addition of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU" +msgid "Removal of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" msgstr "" #: html/addvote.php @@ -198,8 +198,9 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." msgstr "" #: html/home.php @@ -214,7 +215,7 @@ msgid "Remember to vote for your favourite packages!" msgstr "Не заборави да гласаш за омиљене пакете!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." +msgid "Some packages may be provided as binaries in [extra]." msgstr "" #: html/home.php @@ -264,7 +265,7 @@ msgstr "" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -307,9 +308,10 @@ msgstr "" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -321,8 +323,8 @@ msgstr "" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -523,7 +525,7 @@ msgid "Delete" msgstr "" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." +msgid "Only Package Maintainers and Developers can delete packages." msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php @@ -564,7 +566,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -654,7 +656,7 @@ msgid "Merge" msgstr "" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." +msgid "Only Package Maintainers and Developers can merge packages." msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php @@ -712,7 +714,7 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" +msgid "Package Maintainer" msgstr "" #: html/tu.php @@ -724,7 +726,7 @@ msgid "Voting is closed for this proposal." msgstr "" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -1220,7 +1222,7 @@ msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1323,10 +1325,6 @@ msgstr "" msgid "Normal user" msgstr "" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "" @@ -1399,6 +1397,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1823,22 +1830,22 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2099,7 +2106,7 @@ msgid "Registered Users" msgstr "" #: template/stats/general_stats_table.php -msgid "Trusted Users" +msgid "Package Maintainers" msgstr "" #: template/stats/updates_table.php @@ -2285,7 +2292,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2339,3 +2346,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/sv_SE.po b/po/sv_SE.po index 6d09e207..651b8bfb 100644 --- a/po/sv_SE.po +++ b/po/sv_SE.po @@ -4,18 +4,19 @@ # # Translators: # Johannes Löthberg , 2015-2016 +# Kevin Morris , 2022 # Kim Svensson , 2011 # Kim Svensson , 2012 -# Luna Jernberg , 2021 +# Luna Jernberg , 2021-2023 # Robin Björnsvik , 2014-2015 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Swedish (Sweden) (http://www.transifex.com/lfleischer/aurweb/language/sv_SE/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Luna Jernberg , 2021-2023\n" +"Language-Team: Swedish (Sweden) (http://app.transifex.com/lfleischer/aurweb/language/sv_SE/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -36,17 +37,17 @@ msgstr "Notera" #: html/404.php msgid "Git clone URLs are not meant to be opened in a browser." -msgstr "" +msgstr "git clone URLs är inte avsedda att öppnas i en webbläsare." #: html/404.php #, php-format msgid "To clone the Git repository of %s, run %s." -msgstr "" +msgstr "För att klona Git-förrådet för %s, kör %s." #: html/404.php #, php-format msgid "Click %shere%s to return to the %s details page." -msgstr "" +msgstr "Klicka %shär%s för att återgå till %sdetaljsidan." #: html/503.php msgid "Service Unavailable" @@ -136,16 +137,16 @@ msgid "Type" msgstr "Typ" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Tillägg av en TU" +msgid "Addition of a Package Maintainer" +msgstr "Tillägg av en paketunderhållare" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Borttagning av en TU" +msgid "Removal of a Package Maintainer" +msgstr "Borttagning av en paketunderhållare" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Borttagning av en TU (odeklarerad inaktivitet)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "Borttagning av en paketunderhållare (odeklarerad inaktivitet)" #: html/addvote.php msgid "Amendment of Bylaws" @@ -169,7 +170,7 @@ msgstr "Redigera kommentar" #: html/home.php template/header.php msgid "Dashboard" -msgstr "" +msgstr "Informationspanel" #: html/home.php template/header.php msgid "Home" @@ -197,14 +198,15 @@ msgstr "Sam-ansvariga paket" #: html/home.php msgid "Search for packages I co-maintain" -msgstr "" +msgstr "Sök efter paket jag är sam-ansvarig för" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Välkommen till AUR! Var god och läs %sAUR User Guidelines%s och %sAUR TU Guidelines%s för mer information" +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "Välkommen till AUR! Vänligen läs %sAUR användarriktlinjer%s för mer information och %sAUR Inlämningsriktlinjer%s om du vill bidra med en PKGBUILD." #: html/home.php #, php-format @@ -218,8 +220,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "Kom ihåg att rösta på dina favorit paket!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Några paket kan vara tillhandahållna som binära filer i [community]." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Några paket kan vara tillhandahållna som binära filer i [extra]." #: html/home.php msgid "DISCLAIMER" @@ -268,8 +270,8 @@ msgstr "Raderings förfrågan" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "Begär att ett paket ska bli borttaget från Arch User Repository. Snälla använd inte detta om packetet bara är trasigt och kan enkelt bli fixat. Kontakta paket ansvarige istället, och begär att paketet ska bli herrelöst" +"the maintainer and file orphan request if necessary." +msgstr "Begär att ett paket tas bort från Arch User Repository. Vänligen använd inte detta om ett paket är trasigt och lätt kan fixas. Kontakta istället underhållaren och lämna in begäran om ett föräldralöst paket vid behov." #: html/home.php msgid "Merge Request" @@ -311,10 +313,11 @@ msgstr "Diskussion" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "Generella diskussioner om Arch User Repository (AUR) och Trusted User strukturen tar plats på %saur-general%s. För diskussioner om utvecklingen av AUR web interfacet, använd %saur-dev%s maillistan." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "Allmän diskussion om Arch User Repository (AUR) och Paketunderhållar-strukturen äger rum på %saur-general%s. För diskussion som rör utvecklingen om AUR-webbgränssnittet, använd %saur-dev%s mailinglistan." #: html/home.php msgid "Bug Reporting" @@ -325,9 +328,9 @@ msgstr "Bugg Rapportering" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "Om du hittar en bug i AUR webbgränssnittet, var snäll och fyll i en rapport i vårt %särendehanteringssystem%s. Använd systemet %sbara%s för buggar i webbgränssnittet. För att raportera paketbuggar kontakta den som är ansvarig för paketet istället, eller lämna en kommentar på paketets sida." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "Om du hittar ett fel i AUR-webbgränssnittet, vänligen fyll i en felrapport på våran %sfelspårare%s. Använd felspåraren för att rapportera buggar i AUR-webbgränssnittet %sendast%s. För att rapportera paketbuggar kontakta underhållaren eller lämna en kommentar på lämplig paketsida." #: html/home.php msgid "Package Search" @@ -459,7 +462,7 @@ msgstr "Fortsätt" msgid "" "If you have forgotten the user name and the primary e-mail address you used " "to register, please send a message to the %saur-general%s mailing list." -msgstr "" +msgstr "Om du har glömt användarnamnet och den primära e-postadress som du använde för att registrera dig, vänligen skicka ett meddelande till %saur-general%s e-postlistan." #: html/passreset.php msgid "Enter your user name or your primary e-mail address:" @@ -479,7 +482,7 @@ msgstr "De valda paketen har inte gjorts herrelösa, kryssa i konfirmationsrutan msgid "" "The selected packages have not been adopted, check the confirmation " "checkbox." -msgstr "" +msgstr "De valda paketen har inte adopteras, markera kryssrutan för bekräftelse." #: html/pkgbase.php lib/pkgreqfuncs.inc.php msgid "Cannot find package to merge votes and comments into." @@ -527,8 +530,8 @@ msgid "Delete" msgstr "Radera" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Bara Trusted Users och Developers kan radera paket." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "Endast paketunderhållare och utvecklare kan ta bort paket." #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -545,7 +548,7 @@ msgstr "Använd det här formuläret för att göra paket basen %s%s%s och de f msgid "" "By selecting the checkbox, you confirm that you want to no longer be a " "package co-maintainer." -msgstr "" +msgstr "Genom att markera kryssrutan bekräftar du att du inte längre vill vara sam-ansvarig för paket." #: html/pkgdisown.php #, php-format @@ -568,8 +571,8 @@ msgid "Disown" msgstr "Gör herrelös" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "Bara Trusted Users och Developers kan göra paket herrelösa." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "Endast Paketunderhållare och utvecklare kan göra paket herrelösa" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -585,7 +588,7 @@ msgid "" " package version in the AUR does not match the most recent commit. Flagging " "this package should only be done if the sources moved or changes in the " "PKGBUILD are required because of recent upstream changes." -msgstr "" +msgstr "Detta verkar vara ett VCS-paket. Flagga den %sinte%s som inaktuell om paketversionen i AUR inte matchar den mest senaste commit. Flaggning av detta paket bör endast göras om källkoden har flyttats eller ändringar i PKGBUILD krävs på grund av de senaste uppströmsändringarna." #: html/pkgflag.php #, php-format @@ -658,8 +661,8 @@ msgid "Merge" msgstr "Slå ihop" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Bara Trusted Users och Developers kan slå ihop paket." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "Endast paketunderhållare och utvecklare kan slå samman paket." #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -716,8 +719,8 @@ msgid "I accept the terms and conditions above." msgstr "Jag accepterar villkoren ovan." #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Trusted User" +msgid "Package Maintainer" +msgstr "Paketunderhållare" #: html/tu.php msgid "Could not retrieve proposal details." @@ -728,8 +731,8 @@ msgid "Voting is closed for this proposal." msgstr "Röstning för detta förslag är stängt." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Bara Trusted Users är tillåtna att rösta." +msgid "Only Package Maintainers are allowed to vote." +msgstr "Endast paketunderhållare tillåts rösta." #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -974,7 +977,7 @@ msgstr "Paket detaljer kunde inte hittas." #: aurweb/routers/auth.py msgid "Bad Referer header." -msgstr "" +msgstr "Dåligt referenshuvud." #: aurweb/routers/packages.py msgid "You did not select any packages to be notified about." @@ -982,19 +985,19 @@ msgstr "Du har inte valt några paket att notifieras om." #: aurweb/routers/packages.py msgid "The selected packages' notifications have been enabled." -msgstr "" +msgstr "De valda paket aviseringar har aktiverats." #: aurweb/routers/packages.py msgid "You did not select any packages for notification removal." -msgstr "" +msgstr "Du har inte valt några paket för notifieringsborttagning." #: aurweb/routers/packages.py msgid "A package you selected does not have notifications enabled." -msgstr "" +msgstr "Ett paket du valde har inga notifieringar aktiverade." #: aurweb/routers/packages.py msgid "The selected packages' notifications have been removed." -msgstr "" +msgstr "De valda paketens notifieringar har blivit borttagna." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can flag packages." @@ -1046,7 +1049,7 @@ msgstr "Du måste vara inloggad före du kan adoptera paket." #: aurweb/routers/package.py msgid "You are not allowed to adopt one of the packages you selected." -msgstr "" +msgstr "Du är inte tillåten att adoptera ett av paketen du valde." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can disown packages." @@ -1054,7 +1057,7 @@ msgstr "Du måste vara inloggad före du kan göra paket härrelösa." #: aurweb/routers/packages.py msgid "You are not allowed to disown one of the packages you selected." -msgstr "" +msgstr "Du har inte tillåtelse att göra ett av paketen du valde herrelöst." #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to adopt." @@ -1224,8 +1227,8 @@ msgstr "Developer" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "Trusted User & Developer" +msgid "Package Maintainer & Developer" +msgstr "Paketunderhållare & Utvecklare" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1327,10 +1330,6 @@ msgstr "Ditt användarnamn är det namn du kommer att använda för att logga in msgid "Normal user" msgstr "Normal användare" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Trusted user" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Konto avstängt" @@ -1354,7 +1353,7 @@ msgid "" "If you do not hide your email address, it is visible to all registered AUR " "users. If you hide your email address, it is visible to members of the Arch " "Linux staff only." -msgstr "" +msgstr "Om du inte döljer din e-postadress är den synlig för alla registrerade AUR-användare. Om du döljer din e-postadress är den endast synlig för medlemmar av Arch Linux-personalen." #: template/account_edit_form.php msgid "Backup Email Address" @@ -1370,14 +1369,14 @@ msgstr "Ange eventuellt en sekundär e-postadress som kan användas för att åt msgid "" "Password reset links are always sent to both your primary and your backup " "email address." -msgstr "" +msgstr "Länkar för återställning av lösenord skickas alltid till både din primära och din reserv epostadress." #: template/account_edit_form.php #, php-format msgid "" "Your backup email address is always only visible to members of the Arch " "Linux staff, independent of the %s setting." -msgstr "" +msgstr "Din backup-e-postadress är alltid endast synlig för medlemmar av Arch Linux-personalen, oberoende av %s inställningen." #: template/account_edit_form.php msgid "Language" @@ -1403,6 +1402,15 @@ msgid "" " the Arch User Repository." msgstr "Den följande informationen är bara nödvändig om du vill ladda upp paket till Arch User Repository." +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "Specificera flera SSH-nycklar separerade med en ny rad, tomma rader ignoreras." + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "Dölj borttagna kommentarer" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "SSH publik nyckel" @@ -1435,7 +1443,7 @@ msgstr "Ditt nuvarande lösenord" msgid "" "To protect the AUR against automated account creation, we kindly ask you to " "provide the output of the following command:" -msgstr "" +msgstr "För att skydda AUR mot automatiskt kontoskapande ber vi dig att tillhandahålla utdata från följande kommando:" #: template/account_edit_form.php msgid "Answer" @@ -1654,7 +1662,7 @@ msgstr "Lägg till kommentar" msgid "" "Git commit identifiers referencing commits in the AUR package repository and" " URLs are converted to links automatically." -msgstr "" +msgstr "Git commit-identifierare som refererar till commits i AUR-paketförrådet och URL:er konverteras automatiskt till länkar." #: template/pkg_comment_form.php #, php-format @@ -1826,26 +1834,26 @@ msgstr "Slå ihop i" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "" +msgstr "Genom att skicka en begäran om borttagning ber du en paketunderhållare att ta bort paketbasen. Denna typ av begäran bör användas för dubbletter, programvara som överges av uppström, såväl som olagliga och irreparabelt trasiga paket." #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "" +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "Genom att skicka in en sammanslagningsförfrågan ber du en paketunderhållare att ta bort paketbasen och överföra dess röster och kommentarer till en annan paketbas. Att slå samman ett paket påverkar inte motsvarande Git-förråd. Se till att du själv uppdaterar Git-historiken för målpaketet." #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "" +msgstr "Genom att skicka in en föräldralös begäran ber du en paketunderhållare att göra paketbasen herrelös. Vänligen gör bara detta om paketet behöver underhållsåtgärder, underhållaren är MIA och du redan försökt kontakta underhållaren tidigare." #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -1907,7 +1915,7 @@ msgstr "Stäng" #: template/pkgreq_results.php msgid "Pending" -msgstr "" +msgstr "Väntar på" #: template/pkgreq_results.php msgid "Closed" @@ -2026,7 +2034,7 @@ msgstr "Version" msgid "" "Popularity is calculated as the sum of all votes with each vote being " "weighted with a factor of %.2f per day since its creation." -msgstr "" +msgstr "Populäritet räknas ut som summan av alla röster, och alla röster har en vikt med en faktor av %.2f per dag sedan den skapades. " #: template/pkg_search_results.php template/tu_details.php #: template/tu_list.php @@ -2098,8 +2106,8 @@ msgid "Registered Users" msgstr "Registrerade användare" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Trusted Users" +msgid "Package Maintainers" +msgstr "Paketunderhållare" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2180,18 +2188,18 @@ msgid "" "A password reset request was submitted for the account {user} associated " "with your email address. If you wish to reset your password follow the link " "[1] below, otherwise ignore this message and nothing will happen." -msgstr "" +msgstr "En begäran om lösenordsåterställning skickades för kontot {user} som är kopplat till din e-postadress. Om du vill återställa ditt lösenord, följ länken [1] nedan, annars ignorera detta meddelande och ingenting kommer att hända." #: scripts/notify.py msgid "Welcome to the Arch User Repository" -msgstr "" +msgstr "Välkommen till Arch User Repository" #: scripts/notify.py msgid "" "Welcome to the Arch User Repository! In order to set an initial password for" " your new account, please click the link [1] below. If the link does not " "work, try copying and pasting it into your browser." -msgstr "" +msgstr "Välkommen till Arch User Repository! För att ställa in ett första lösenord för ditt nya konto, klicka på länken [1] nedan. Om länken inte fungerar, försök att kopiera och klistra in den i din webbläsare." #: scripts/notify.py #, python-brace-format @@ -2208,7 +2216,7 @@ msgstr "{user} [1] lade till följande kommentar till {pkgbase} [2]:" msgid "" "If you no longer wish to receive notifications about this package, please go" " to the package page [2] and select \"{label}\"." -msgstr "" +msgstr "Om du inte längre vill få meddelanden om detta paket, gå till paketsidan [2] och välj \"{label}\"." #: scripts/notify.py #, python-brace-format @@ -2218,47 +2226,47 @@ msgstr "AUR paket uppdatering: {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "{user} [1] pushed a new commit to {pkgbase} [2]." -msgstr "" +msgstr "{user} [1] knuffade en ny commit till {pkgbase} [2]." #: scripts/notify.py #, python-brace-format msgid "AUR Out-of-date Notification for {pkgbase}" -msgstr "" +msgstr "AUR inaktuell avisering för {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "Your package {pkgbase} [1] has been flagged out-of-date by {user} [2]:" -msgstr "" +msgstr "Ditt paket {pkgbase} [1] har flaggats som inaktuellt av {user} [2]:" #: scripts/notify.py #, python-brace-format msgid "AUR Ownership Notification for {pkgbase}" -msgstr "" +msgstr "AUR-ägarskapsmeddelande för {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was adopted by {user} [2]." -msgstr "" +msgstr "Paketet {pkgbase} [1] adopterades av {user} [2]." #: scripts/notify.py #, python-brace-format msgid "The package {pkgbase} [1] was disowned by {user} [2]." -msgstr "" +msgstr "Paketet {pkgbase} [1] gjordes herrelöst av {user} [2]." #: scripts/notify.py #, python-brace-format msgid "AUR Co-Maintainer Notification for {pkgbase}" -msgstr "" +msgstr "AUR sam-ansvarig meddelande för {pkgbase}" #: scripts/notify.py #, python-brace-format msgid "You were added to the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "Du blev tillagd till sam-ansvarig listan för {pkgbase} [1]." #: scripts/notify.py #, python-brace-format msgid "You were removed from the co-maintainer list of {pkgbase} [1]." -msgstr "" +msgstr "Du blev borttagen från sam-ansvarig listan för {pkgbase} [1]." #: scripts/notify.py #, python-brace-format @@ -2272,7 +2280,7 @@ msgid "" "\n" "-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." -msgstr "" +msgstr "{user} [1] slog ihop {old} [2] till {new} [3].\n\n--\nOm du inte längre vill få meddelanden om det nya paketet, gå till [3] och klicka på \"{label}\"." #: scripts/notify.py #, python-brace-format @@ -2280,19 +2288,19 @@ msgid "" "{user} [1] deleted {pkgbase} [2].\n" "\n" "You will no longer receive notifications about this package." -msgstr "" +msgstr "{user} [1] raderade {pkgbase} [2].\n\nDu kommer inte längre att få aviseringar om detta paket." #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" -msgstr "" +msgid "Package Maintainer Vote Reminder: Proposal {id}" +msgstr "Paketunderhållare röstningspåminnelse Förslag {id}" #: scripts/notify.py #, python-brace-format msgid "" "Please remember to cast your vote on proposal {id} [1]. The voting period " "ends in less than 48 hours." -msgstr "" +msgstr "Kom ihåg att lägga din röst på förslaget {id} [1]. Omröstningsperioden slutar om mindre än 48 timmar." #: aurweb/routers/accounts.py msgid "Invalid account type provided." @@ -2308,33 +2316,65 @@ msgstr "Du har inte behörighet att ändra den här användarens kontotyp till % #: aurweb/packages/requests.py msgid "No due existing orphan requests to accept for %s." -msgstr "" +msgstr "Inga befintliga herrelösa begäranden att acceptera för %s." #: aurweb/asgi.py msgid "Internal Server Error" -msgstr "" +msgstr "Internt serverfel" #: templates/errors/500.html msgid "A fatal error has occurred." -msgstr "" +msgstr "Ett allvarligt fel har inträffat." #: templates/errors/500.html msgid "" "Details have been logged and will be reviewed by the postmaster posthaste. " "We apologize for any inconvenience this may have caused." -msgstr "" +msgstr "Detaljer har loggats och kommer att granskas av postmästaren så snabbt som möjligt. Vi ber om ursäkt för eventuella besvär som detta kan ha orsakat." #: aurweb/scripts/notify.py msgid "AUR Server Error" -msgstr "" +msgstr "AUR serverfel" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "Relaterade paketförfrågningar stängningskommentar..." #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." -msgstr "" +msgstr "Denna åtgärd kommer att stänga alla väntande paketförfrågningar relaterade till det paketet. %sOm kommentarer%s utelämnas kommer en stängningskommentar att automatiskt genereras." + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "tilldelade" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "Visa %d mer" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "beroenden" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "Kontot har inte raderas, markera kryssrutan för bekräftelse." + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "Avbryt" + +#: templates/requests.html +msgid "Package name" +msgstr "Paketnamn" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "Observera att om du döljer din e-postadress hamnar den på BCC-listan för eventuella begärandemeddelanden. Om någon svarar på dessa aviseringar kommer du inte att få något e-postmeddelande. Svaren skickas dock vanligtvis till e-postlistan och kommer då att vara synliga i arkivet." diff --git a/po/tr.po b/po/tr.po index 83b1e4df..8742b919 100644 --- a/po/tr.po +++ b/po/tr.po @@ -5,7 +5,7 @@ # Translators: # tarakbumba , 2011,2013-2015 # tarakbumba , 2012,2014 -# Demiray Muhterem , 2015,2020-2021 +# Demiray Muhterem , 2015,2020-2022 # Koray Biçer , 2020 # Lukas Fleischer , 2011 # Samed Beyribey , 2012 @@ -15,11 +15,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Turkish (http://www.transifex.com/lfleischer/aurweb/language/tr/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Demiray Muhterem , 2015,2020-2022\n" +"Language-Team: Turkish (http://app.transifex.com/lfleischer/aurweb/language/tr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -140,16 +140,16 @@ msgid "Type" msgstr "Tür" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Bir GK ekleme" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Bir GK çıkartma" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Bir GK çıkartma (bildirilmemiş hareketsizlik)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -206,9 +206,10 @@ msgstr "Birlikte baktığım paketleri ara" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "AUR'a hoş geldiniz! Bilgi almak için lütfen %sAUR Kullanıcı Rehberi%s ve %sGK Rehberini%s okuyun." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -222,8 +223,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "Beğendiğiniz paketleri oylamayı unutmayın!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Burada listelenen paketlerin bazıları [community] deposunda yer almaktadır." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Burada listelenen paketlerin bazıları [extra] deposunda yer almaktadır." #: html/home.php msgid "DISCLAIMER" @@ -272,8 +273,8 @@ msgstr "Silme Talebi" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "Bir paketin Arch Kullanıcı Deposundan kaldırılması talebidir. Lütfen bunu, bir paket çalışmıyor fakat kolayca düzeltilebiliyorsa kullanmayın. Bunun yerine, paket bakımcısı ile iletişim kurun ve mecbur kalınırsa paketin sahipsiz bırakılması talebinde bulunun." +"the maintainer and file orphan request if necessary." +msgstr "" #: html/home.php msgid "Merge Request" @@ -315,10 +316,11 @@ msgstr "Tartışma" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "Arch Kullanıcı Deposu (AUR) ve Güvenilir Kullanıcı yapısı ile ilgili genel tartışmalar %saur-general%s üzerinde yapılır. AUR web arayüzü geliştirme süreci ilgili tartışmalar %saur-dev%s listesinde yapılmaktadır." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -329,9 +331,9 @@ msgstr "Hata Bildirimi" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "AUR web arayüzünde bir hata bulursanız, lütfen %s hata izleyicimiz %s üzerinde bir hata raporu doldurun. AUR web arayüzündeki %s hataları raporlamak için sadece %s izleyiciyi kullanın . Paketleme hatalarını bildirmek için paket bakıcısına başvurun veya ilgili paket sayfasına yorum yapın." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "" #: html/home.php msgid "Package Search" @@ -531,8 +533,8 @@ msgid "Delete" msgstr "Sil" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Sadece geliştiriciler ve güvenilir kullanıcılar paket silebilir." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -572,8 +574,8 @@ msgid "Disown" msgstr "Sorumluluğunu bırak" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "Sadece geliştiriciler ve güvenilir kullanıcılar paketleri sahipsiz bırakabilir." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -662,8 +664,8 @@ msgid "Merge" msgstr "Birleştir" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Sadece geliştiriciler ve güvenilir kullanıcılar paket birleştirebilir." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -720,8 +722,8 @@ msgid "I accept the terms and conditions above." msgstr "Yukarıdaki şartlar ve koşulları kabul ediyorum." #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Güvenilen Kullanıcı" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -732,8 +734,8 @@ msgid "Voting is closed for this proposal." msgstr "Bu öneri için oylama kapanmıştır." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Sadece Güvenilir Kullanıcılar oy kullanabilir." +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -1228,8 +1230,8 @@ msgstr "Geliştirici" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "Güvenilir Kullanıcı & Geliştirici" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1331,10 +1333,6 @@ msgstr "Kullanıcı adınız, oturum açmak için kullanacağınız addır. Hesa msgid "Normal user" msgstr "Normal kullanıcı" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Güvenilen kullanıcı" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Hesap Donduruldu" @@ -1407,6 +1405,15 @@ msgid "" " the Arch User Repository." msgstr "Aşağıdaki bilgi sadece Arch Kullanıcı Deposu' na paket göndermek istiyorsanız gereklidir." +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "SSH Kamu Anahtarı" @@ -1830,26 +1837,26 @@ msgstr "Şununla ilişkilendir:" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "Silme talebi göndererek, Güvenilir Kullanıcıdan paketi silmesini istiyorsunuz. Bu tür bir istek birden fazla olan paketlerde, geliştirilmesi durdurulmuş yazılımlarda, ve aynı zamanda yasadışı ve onarılamaz bozuklukta olan paketler için kullanılmalıdır." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "Birleştirme talebi göndererek, Güvenilir Kullanıcıdan paketi silmesini ve bu paketin oylarını ve yorumlarını diğer pakete transfer etmesini istiyorsunuz. Bir paketi birleştirmek ilgili Git deposunu etkilemeyecektir. Hedef paketin Git geçmişini bizzat güncellediğinizden emin olun. " +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "Bir sahipsiz istek göndererek, Güvenilir Kullanıcıdan paket tabanını reddetmesini istersiniz. Lütfen bunu yalnızca paketin sürdürme eylemine ihtiyacı varsa, sürdürücü MIA ise ve daha önce bakım görevlisine başvurmayı denediyseniz yapın." +msgstr "" #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -2102,8 +2109,8 @@ msgid "Registered Users" msgstr "Kayıtlı Kullanıcılar" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Güvenilen Kullanıcılar" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2288,8 +2295,8 @@ msgstr "{user} [1] {pkgbase} [2] paketini sildi.\n\nArtık bu paket hakkında bi #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" -msgstr "TU Oylama Hatırlatıcısı: Teklif {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" +msgstr "" #: scripts/notify.py #, python-brace-format @@ -2316,29 +2323,61 @@ msgstr "%s için kabul edilecek sahipsize gereksinim yok." #: aurweb/asgi.py msgid "Internal Server Error" -msgstr "" +msgstr "İç Sunucu Hatası" #: templates/errors/500.html msgid "A fatal error has occurred." -msgstr "" +msgstr "Önemli bir hata oluştu." #: templates/errors/500.html msgid "" "Details have been logged and will be reviewed by the postmaster posthaste. " "We apologize for any inconvenience this may have caused." -msgstr "" +msgstr "Ayrıntılar günlüğe kaydedildi ve posta yöneticisi tarafından gözden geçirilecek. Bunun neden olabileceği rahatsızlıktan dolayı özür dileriz." #: aurweb/scripts/notify.py msgid "AUR Server Error" -msgstr "" +msgstr "AUR Sunucu Hatası" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "İlgili paket isteği kapatma yorumları..." #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." +msgstr "Bu eylem, kendisiyle ilgili bekleyen paket isteklerini kapatacaktır. %s Yorum %s atlanırsa, bir kapatma yorumu otomatik olarak oluşturulur." + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." msgstr "" diff --git a/po/uk.po b/po/uk.po index a4410185..040c5085 100644 --- a/po/uk.po +++ b/po/uk.po @@ -7,16 +7,16 @@ # Rax Garfield , 2012 # Rax Garfield , 2012 # Vladislav Glinsky , 2019 -# Yarema aka Knedlyk , 2011-2018 +# Yarema aka Knedlyk , 2011-2018,2022 # Данило Коростіль , 2011 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Ukrainian (http://www.transifex.com/lfleischer/aurweb/language/uk/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: Yarema aka Knedlyk , 2011-2018,2022\n" +"Language-Team: Ukrainian (http://app.transifex.com/lfleischer/aurweb/language/uk/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -80,7 +80,7 @@ msgstr "У вас недостатньо прав для редагування #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." -msgstr "" +msgstr "Неправильний пароль" #: html/account.php msgid "Use this form to search existing accounts." @@ -137,16 +137,16 @@ msgid "Type" msgstr "Тип" #: html/addvote.php -msgid "Addition of a TU" -msgstr "Додавання довіреного користувача" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "Вилучення довіреного користувача" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "Вилучення довіреного користувача (неоголошена бездіяльність)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -203,9 +203,10 @@ msgstr "Пошук пакунків з сумісним супроводом" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "Раді вітати вас в «AUR» — у сховищі користувацьких пакунків. Розширена довідка надана в %sінструкції користувача AUR%s та %sінструкції довіреного користувача (TU) AUR%s." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -219,8 +220,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "Не забудьте проголосувати за улюблені пакунки!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "Деякі пакунки можуть бути в бінарному вигляді у сховищі [community]." +msgid "Some packages may be provided as binaries in [extra]." +msgstr "Деякі пакунки можуть бути в бінарному вигляді у сховищі [extra]." #: html/home.php msgid "DISCLAIMER" @@ -269,8 +270,8 @@ msgstr "Запит щодо вилучення" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "Запит щодо вилучення пакунку зі сховища користувацьких пакунків AUR. Будь ласка, не використовуйте його, якщо пакунок містить проблеми, які можна легко виправити. Натомість зв’яжіться з супровідником пакунку та в разі необхідності зробіть запит покинути пакунок." +"the maintainer and file orphan request if necessary." +msgstr "" #: html/home.php msgid "Merge Request" @@ -312,10 +313,11 @@ msgstr "Обговорення" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "Загальне обговорення сховища користувацьких пакунків (AUR) та структури довірених користувачів відбувається в %saur-general%s. Для дискусій про розробку AUR використовуйте список розсилання %saur-dev%s." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -326,9 +328,9 @@ msgstr "Повідомлення про вади" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "Якщо Ви знайдете ваду у веб-інтерфейсі AUR, повідомте про це нас на нашому %sтрекері вад%s. Таким чином слід сповіщати %sлише%s про проблеми у веб-інтерфейсі AUR. Про вади пакунка зв’яжіться з його супровідником або залиште коментар на відповідній сторінці цього пакунка." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "" #: html/home.php msgid "Package Search" @@ -377,7 +379,7 @@ msgstr "Увійдіть, ввівши облікові дані." #: html/login.php msgid "User name or primary email address" -msgstr "" +msgstr "Назва користувача або адреса електронної пошти" #: html/login.php template/account_delete.php template/account_edit_form.php msgid "Password" @@ -441,7 +443,7 @@ msgstr "Ваш пароль успішно скинуто." #: html/passreset.php msgid "Confirm your user name or primary e-mail address:" -msgstr "" +msgstr "Підтвердити назву користувача або адреса електронної пошти:" #: html/passreset.php msgid "Enter your new password:" @@ -460,11 +462,11 @@ msgstr "Продовжити" msgid "" "If you have forgotten the user name and the primary e-mail address you used " "to register, please send a message to the %saur-general%s mailing list." -msgstr "" +msgstr "Якщо Ви забули назву користувача і адресу електронної пошти, використану при реєстрації, зверніться до списку розсилання %saur-general%s." #: html/passreset.php msgid "Enter your user name or your primary e-mail address:" -msgstr "" +msgstr "Введіть назву користувача або адресу електронної пошти:" #: html/pkgbase.php msgid "Package Bases" @@ -480,7 +482,7 @@ msgstr "Вибрані пакунки все ще мають власника, msgid "" "The selected packages have not been adopted, check the confirmation " "checkbox." -msgstr "" +msgstr "Обрані пакунки не прийнято, перевірте, чи поставлено галочку в полі підтвердження." #: html/pkgbase.php lib/pkgreqfuncs.inc.php msgid "Cannot find package to merge votes and comments into." @@ -528,8 +530,8 @@ msgid "Delete" msgstr "Вилучити" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "Тільки Довірені Користувачі та Розробники можуть вилучати пакунки." +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -569,8 +571,8 @@ msgid "Disown" msgstr "Відректися" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "Тільки Довірені Користувачі та Розробники можуть забирати права власності на пакунок." +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -586,7 +588,7 @@ msgid "" " package version in the AUR does not match the most recent commit. Flagging " "this package should only be done if the sources moved or changes in the " "PKGBUILD are required because of recent upstream changes." -msgstr "" +msgstr "Здається, це пакет VCS. Будь ласка, %sне%s позначайте його як застарілий, якщо версія пакета в AUR не відповідає останньому коміту. Позначити цей пакунок слід лише в тому випадку, якщо джерела переміщено або потрібні зміни в PKGBUILD в зв'язку з останніми змінами." #: html/pkgflag.php #, php-format @@ -659,8 +661,8 @@ msgid "Merge" msgstr "Об’єднати" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "Тільки Довірені Користувачі та Розробники можуть об’єднувати пакунки." +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -717,8 +719,8 @@ msgid "I accept the terms and conditions above." msgstr "Я приймаю подані вище терміни і умови. " #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "Довірений користувач" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -729,8 +731,8 @@ msgid "Voting is closed for this proposal." msgstr "Голосування на цю пропозицію закрито." #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "Тільки Довірені Користувачі мають право голосу." +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -785,7 +787,7 @@ msgstr "Може містити тільки один період, підкре #: lib/acctfuncs.inc.php msgid "Please confirm your new password." -msgstr "" +msgstr "Підтвердіть новий пароль, будь ласка." #: lib/acctfuncs.inc.php msgid "The email address is invalid." @@ -793,7 +795,7 @@ msgstr "Адреса електронної пошти неправильна." #: lib/acctfuncs.inc.php msgid "The backup email address is invalid." -msgstr "" +msgstr "Неправильна адреса електронної пошти для відновлення." #: lib/acctfuncs.inc.php msgid "The home page is invalid, please specify the full HTTP(s) URL." @@ -836,15 +838,15 @@ msgstr "Публічний ключ SSH, %s%s%s, вже використовує #: lib/acctfuncs.inc.php msgid "The CAPTCHA is missing." -msgstr "" +msgstr "Пропущено CAPTCHA." #: lib/acctfuncs.inc.php msgid "This CAPTCHA has expired. Please try again." -msgstr "" +msgstr "Термін дії цієї CAPTCHA закінчився. Будь ласка, спробуйте ще раз." #: lib/acctfuncs.inc.php msgid "The entered CAPTCHA answer is invalid." -msgstr "" +msgstr "Введена відповідь CAPTCHA недійсна." #: lib/acctfuncs.inc.php #, php-format @@ -886,7 +888,7 @@ msgstr "Обліковий запис вилучено" #: aurweb/routers/accounts.py msgid "You do not have permission to suspend accounts." -msgstr "" +msgstr " \nВи не маєте дозволу на призупинення облікових записів." #: lib/acctfuncs.inc.php #, php-format @@ -975,27 +977,27 @@ msgstr "Інформації про пакунок не знайдено." #: aurweb/routers/auth.py msgid "Bad Referer header." -msgstr "" +msgstr "Поганий заголовок Referer." #: aurweb/routers/packages.py msgid "You did not select any packages to be notified about." -msgstr "" +msgstr "Ви не вибрали жодних пакунків, про які потрібно сповіщати." #: aurweb/routers/packages.py msgid "The selected packages' notifications have been enabled." -msgstr "" +msgstr "Сповіщення для вибраних пакунків увімкнено." #: aurweb/routers/packages.py msgid "You did not select any packages for notification removal." -msgstr "" +msgstr "Ви не вибрали жодних пакунків для видалення сповіщень." #: aurweb/routers/packages.py msgid "A package you selected does not have notifications enabled." -msgstr "" +msgstr "У вибраному вами пакунку не ввімкнено сповіщення." #: aurweb/routers/packages.py msgid "The selected packages' notifications have been removed." -msgstr "" +msgstr "Сповіщення для вибраних пакунків видалено." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can flag packages." @@ -1035,7 +1037,7 @@ msgstr "Не вибрано жодного пакунку для вилучен #: aurweb/routers/packages.py msgid "One of the packages you selected does not exist." -msgstr "" +msgstr "Один із вибраних Вами пакунків не існує." #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been deleted." @@ -1047,7 +1049,7 @@ msgstr "Для перейняття пакунків слід увійти." #: aurweb/routers/package.py msgid "You are not allowed to adopt one of the packages you selected." -msgstr "" +msgstr "У Вас немає дозволу прийняти один з вибраних Вами пакунків." #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can disown packages." @@ -1055,7 +1057,7 @@ msgstr "Для зречення пакунків слід увійти." #: aurweb/routers/packages.py msgid "You are not allowed to disown one of the packages you selected." -msgstr "" +msgstr "У Вас немає дозволу відмовитися від одного з вибраних Вами пакунків" #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to adopt." @@ -1225,8 +1227,8 @@ msgstr "Розробник" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "Довірений користувач & Розробник" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1297,7 +1299,7 @@ msgstr "Редагувати обліковий запис цього корис #: template/account_details.php msgid "List this user's comments" -msgstr "" +msgstr "Показати коментарі цього користувача" #: template/account_edit_form.php #, php-format @@ -1312,7 +1314,7 @@ msgstr "Клацніть %sтут%s, щоб дізнатися більше пр #: template/account_edit_form.php #, php-format msgid "Click %shere%s to list the comments made by this account." -msgstr "" +msgstr "Натисніть %sтут%s, щоб показати коментарі, зроблені цим обліковим записом." #: template/account_edit_form.php msgid "required" @@ -1328,10 +1330,6 @@ msgstr "Ваша назва користувача є назвою, що буд msgid "Normal user" msgstr "Звичайний користувач" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "Довіренний користувач" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "Обліковий запис призупинено" @@ -1355,30 +1353,30 @@ msgid "" "If you do not hide your email address, it is visible to all registered AUR " "users. If you hide your email address, it is visible to members of the Arch " "Linux staff only." -msgstr "" +msgstr "Якщо ви не приховаєте свою адресу електронної пошти, тоді її можуть бачити всі зареєстровані користувачі AUR. Якщо Ви приховаєте свою адресу електронної пошти, тоді її зможуть бачити лише співробітники Arch Linux." #: template/account_edit_form.php msgid "Backup Email Address" -msgstr "" +msgstr "Резервна адреса електронної пошти" #: template/account_edit_form.php msgid "" "Optionally provide a secondary email address that can be used to restore " "your account in case you lose access to your primary email address." -msgstr "" +msgstr "За бажанням вкажіть додаткову адресу електронної пошти, яку можна використовувати для відновлення облікового запису на випадок втрати доступу до своєї основної електронної адреси." #: template/account_edit_form.php msgid "" "Password reset links are always sent to both your primary and your backup " "email address." -msgstr "" +msgstr "Посилання для скидання пароля завжди надсилаються як на вашу основну, так і на резервну адресу електронної пошти." #: template/account_edit_form.php #, php-format msgid "" "Your backup email address is always only visible to members of the Arch " "Linux staff, independent of the %s setting." -msgstr "" +msgstr "Вашу резервну електронну адресу завжди бачать лише співробітники Arch Linux, незалежно від налаштувань. %s ." #: template/account_edit_form.php msgid "Language" @@ -1392,7 +1390,7 @@ msgstr "Часова зона" msgid "" "If you want to change the password, enter a new password and confirm the new" " password by entering it again." -msgstr "" +msgstr "Якщо Ви бажаєте змінити пароль, введіть новий пароль і підтвердьте новий пароль, ввівши його ще раз." #: template/account_edit_form.php msgid "Re-type password" @@ -1404,6 +1402,15 @@ msgid "" " the Arch User Repository." msgstr "Наступну інформацію потрібно, якщо Ви бажаєте надіслати пакунки до Сховища Користувацьких Пакунків AUR." +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "Публічний ключ SSH" @@ -1426,21 +1433,21 @@ msgstr "Сповіщення про зміну власника" #: template/account_edit_form.php msgid "To confirm the profile changes, please enter your current password:" -msgstr "" +msgstr "Щоб підтвердити зміни профілю, введіть поточний пароль:" #: template/account_edit_form.php msgid "Your current password" -msgstr "" +msgstr "Ваш поточний пароль" #: template/account_edit_form.php msgid "" "To protect the AUR against automated account creation, we kindly ask you to " "provide the output of the following command:" -msgstr "" +msgstr "Щоб захистити AUR від автоматичного створення облікового запису, ми просимо Вас надати результат такої команди:" #: template/account_edit_form.php msgid "Answer" -msgstr "" +msgstr "Відповідь" #: template/account_edit_form.php template/pkgbase_details.php #: template/pkg_details.php @@ -1605,7 +1612,7 @@ msgstr "тільки для читання" #: template/pkgbase_details.php template/pkg_details.php msgid "click to copy" -msgstr "" +msgstr "натисніть, щоб скопіювати" #: template/pkgbase_details.php template/pkg_details.php #: template/pkg_search_form.php @@ -1657,12 +1664,12 @@ msgstr "Додати коментар" msgid "" "Git commit identifiers referencing commits in the AUR package repository and" " URLs are converted to links automatically." -msgstr "" +msgstr "Відповідні відсилачі комітів до ідентифікаторів комітів Git в сховищі пакунків AUR та URL-адреси автоматично перетворюються на посилання." #: template/pkg_comment_form.php #, php-format msgid "%sMarkdown syntax%s is partially supported." -msgstr "" +msgstr "%sСинтакс Markdown%s підтримується частково." #: template/pkg_comments.php msgid "Pinned Comments" @@ -1674,7 +1681,7 @@ msgstr "Останні коментарі" #: template/pkg_comments.php msgid "Comments for" -msgstr "" +msgstr "Коментарі для" #: template/pkg_comments.php #, php-format @@ -1689,7 +1696,7 @@ msgstr "Анонімний коментар про %s" #: template/pkg_comments.php #, php-format msgid "Commented on package %s on %s" -msgstr "" +msgstr "Коментовано пакунок %s з датою %s" #: template/pkg_comments.php #, php-format @@ -1829,26 +1836,26 @@ msgstr "Об'єднати в" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "Надсилаючи запит на вилучення, Ви просите Довіреного Користувача вилучити пакунок з бази пакунків. Цей тип запиту повинен використовуватися для дублікатів, неоновлюваних програм, а також нелегальних і невиправно пошкоджених пакунків." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "Надсилаючи запит на об'єднання, Ви просите Довіреного Користувача вилучити базу пакунків і перенести всі його голосування і коментарі до іншої бази пакунків. Об'єднання пакунку не впливає на відповідні сховища Git. Впевніться, що Ви самостійно оновили історію Git доцільового пакунку." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "Надсилаючи запит покинути пакунок, Ви просите Довіреного Користувача позбавити базу пакунків власника. Робіть це, лише коли пакунок потребує якоїсь дії від супровідника, супровідник не робить жодних дій і ви вже попередньо намагалися зв'язатися з ним." +msgstr "" #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -2109,8 +2116,8 @@ msgid "Registered Users" msgstr "Зареєстровані користувачі" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "Довірені користувачі" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2283,7 +2290,7 @@ msgid "" "\n" "-- \n" "If you no longer wish receive notifications about the new package, please go to [3] and click \"{label}\"." -msgstr "" +msgstr "{user} [1] з'єднав {old} [2] до {new} [3].\n\n-- \nЯкщо Ви не бажаєте більше отримувати сповіщення про новий пакунок, перейдіть на сторінку [3] і натисніть \"{label}\"." #: scripts/notify.py #, python-brace-format @@ -2295,8 +2302,8 @@ msgstr "{user} [1] вилучив {pkgbase} [2].\n\nВи більше не бу #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" -msgstr "Нагадування про голосування на довіреного користувача: Пропозиція {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" +msgstr "" #: scripts/notify.py #, python-brace-format @@ -2307,45 +2314,77 @@ msgstr "Будь ласка, не забудьте подати свій гол #: aurweb/routers/accounts.py msgid "Invalid account type provided." -msgstr "" +msgstr "Вказано недійсний тип облікового запису." #: aurweb/routers/accounts.py msgid "You do not have permission to change account types." -msgstr "" +msgstr "У Вас немає дозволу змінювати типи облікових записів." #: aurweb/routers/accounts.py msgid "You do not have permission to change this user's account type to %s." -msgstr "" +msgstr "Ви не маєте дозволу змінити тип облікового запису цього користувача на %s." #: aurweb/packages/requests.py msgid "No due existing orphan requests to accept for %s." -msgstr "" +msgstr "Немає наявних запитів на прийняття для %s." #: aurweb/asgi.py msgid "Internal Server Error" -msgstr "" +msgstr "Внутрішня помилка сервера" #: templates/errors/500.html msgid "A fatal error has occurred." -msgstr "" +msgstr "Сталася фатальна помилка." #: templates/errors/500.html msgid "" "Details have been logged and will be reviewed by the postmaster posthaste. " "We apologize for any inconvenience this may have caused." -msgstr "" +msgstr "Подробиці зареєстровані та будуть переглянуті поштмейстером posthaste. Просимо вибачення за можливі незручності." #: aurweb/scripts/notify.py msgid "AUR Server Error" -msgstr "" +msgstr "Помилка сервера AUR" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "Пов'язані коментарі щодо закриття запиту на пакунок..." #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." +msgstr "Ця дія закриє всі запити на пакет, що очікують на розгляд. Якщо %sКоментарі%s пропущено, тоді буде автоматично згенеровано коментар закриття." + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." msgstr "" diff --git a/po/vi.po b/po/vi.po index 3ea5bad3..c3f919c4 100644 --- a/po/vi.po +++ b/po/vi.po @@ -6,11 +6,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Vietnamese (http://www.transifex.com/lfleischer/aurweb/language/vi/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Vietnamese (http://app.transifex.com/lfleischer/aurweb/language/vi/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -131,15 +131,15 @@ msgid "Type" msgstr "" #: html/addvote.php -msgid "Addition of a TU" +msgid "Addition of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU" +msgid "Removal of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" msgstr "" #: html/addvote.php @@ -197,8 +197,9 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." msgstr "" #: html/home.php @@ -213,7 +214,7 @@ msgid "Remember to vote for your favourite packages!" msgstr "" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." +msgid "Some packages may be provided as binaries in [extra]." msgstr "" #: html/home.php @@ -263,7 +264,7 @@ msgstr "" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -306,9 +307,10 @@ msgstr "" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -320,8 +322,8 @@ msgstr "" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -522,7 +524,7 @@ msgid "Delete" msgstr "" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." +msgid "Only Package Maintainers and Developers can delete packages." msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php @@ -563,7 +565,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -653,7 +655,7 @@ msgid "Merge" msgstr "" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." +msgid "Only Package Maintainers and Developers can merge packages." msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php @@ -711,7 +713,7 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" +msgid "Package Maintainer" msgstr "" #: html/tu.php @@ -723,7 +725,7 @@ msgid "Voting is closed for this proposal." msgstr "" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -1219,7 +1221,7 @@ msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1322,10 +1324,6 @@ msgstr "" msgid "Normal user" msgstr "" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "" @@ -1398,6 +1396,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1820,22 +1827,22 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2088,7 +2095,7 @@ msgid "Registered Users" msgstr "" #: template/stats/general_stats_table.php -msgid "Trusted Users" +msgid "Package Maintainers" msgstr "" #: template/stats/updates_table.php @@ -2274,7 +2281,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2328,3 +2335,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/zh.po b/po/zh.po index 04fe06f3..7abbe77e 100644 --- a/po/zh.po +++ b/po/zh.po @@ -6,11 +6,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Chinese (http://www.transifex.com/lfleischer/aurweb/language/zh/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Chinese (http://app.transifex.com/lfleischer/aurweb/language/zh/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -131,15 +131,15 @@ msgid "Type" msgstr "" #: html/addvote.php -msgid "Addition of a TU" +msgid "Addition of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU" +msgid "Removal of a Package Maintainer" msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" msgstr "" #: html/addvote.php @@ -197,8 +197,9 @@ msgstr "" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." msgstr "" #: html/home.php @@ -213,7 +214,7 @@ msgid "Remember to vote for your favourite packages!" msgstr "" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." +msgid "Some packages may be provided as binaries in [extra]." msgstr "" #: html/home.php @@ -263,7 +264,7 @@ msgstr "" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." +"the maintainer and file orphan request if necessary." msgstr "" #: html/home.php @@ -306,9 +307,10 @@ msgstr "" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." msgstr "" #: html/home.php @@ -320,8 +322,8 @@ msgstr "" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." msgstr "" #: html/home.php @@ -522,7 +524,7 @@ msgid "Delete" msgstr "" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." +msgid "Only Package Maintainers and Developers can delete packages." msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php @@ -563,7 +565,7 @@ msgid "Disown" msgstr "" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." +msgid "Only Package Maintainers and Developers can disown packages." msgstr "" #: html/pkgflagcomment.php @@ -653,7 +655,7 @@ msgid "Merge" msgstr "" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." +msgid "Only Package Maintainers and Developers can merge packages." msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php @@ -711,7 +713,7 @@ msgid "I accept the terms and conditions above." msgstr "" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" +msgid "Package Maintainer" msgstr "" #: html/tu.php @@ -723,7 +725,7 @@ msgid "Voting is closed for this proposal." msgstr "" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." +msgid "Only Package Maintainers are allowed to vote." msgstr "" #: html/tu.php @@ -1219,7 +1221,7 @@ msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" +msgid "Package Maintainer & Developer" msgstr "" #: template/account_details.php template/account_edit_form.php @@ -1322,10 +1324,6 @@ msgstr "" msgid "Normal user" msgstr "" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "" @@ -1398,6 +1396,15 @@ msgid "" " the Arch User Repository." msgstr "" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "" @@ -1820,22 +1827,22 @@ msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." @@ -2088,7 +2095,7 @@ msgid "Registered Users" msgstr "" #: template/stats/general_stats_table.php -msgid "Trusted Users" +msgid "Package Maintainers" msgstr "" #: template/stats/updates_table.php @@ -2274,7 +2281,7 @@ msgstr "" #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" msgstr "" #: scripts/notify.py @@ -2328,3 +2335,35 @@ msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." msgstr "" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "" diff --git a/po/zh_CN.po b/po/zh_CN.po index 53d42bc8..f32d628c 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -8,6 +8,7 @@ # dongfengweixiao , 2015 # Felix Yan , 2014,2021 # Feng Chao , 2012,2021 +# lakejason0 , 2022 # Lukas Fleischer , 2011 # pingplug , 2017-2018 # Feng Chao , 2012 @@ -17,11 +18,11 @@ msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Chinese (China) (http://www.transifex.com/lfleischer/aurweb/language/zh_CN/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: lakejason0 , 2022\n" +"Language-Team: Chinese (China) (http://app.transifex.com/lfleischer/aurweb/language/zh_CN/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -42,7 +43,7 @@ msgstr "提示" #: html/404.php msgid "Git clone URLs are not meant to be opened in a browser." -msgstr "Git clone URLs 并不意味着能被浏览器打开。" +msgstr "Git clone URL 并不应该使用浏览器打开。" #: html/404.php #, php-format @@ -65,11 +66,11 @@ msgstr "别慌!本站正在维护中,不久后将恢复。" #: html/account.php msgid "Account" -msgstr "帐户" +msgstr "账户" #: html/account.php template/header.php msgid "Accounts" -msgstr "帐户" +msgstr "账户" #: html/account.php html/addvote.php msgid "You are not allowed to access this area." @@ -81,7 +82,7 @@ msgstr "无法获取指定用户的信息。" #: html/account.php msgid "You do not have permission to edit this account." -msgstr "您没有权限编辑此帐户。" +msgstr "您没有权限编辑此账户。" #: html/account.php lib/acctfuncs.inc.php msgid "Invalid password." @@ -89,11 +90,11 @@ msgstr "密码无效。" #: html/account.php msgid "Use this form to search existing accounts." -msgstr "使用此表单查找存在的帐户。" +msgstr "使用此表单查找存在的账户。" #: html/account.php msgid "You must log in to view user information." -msgstr "您需要登录后才能察看用户信息。" +msgstr "您需要登录后才能查看用户信息。" #: html/addvote.php template/tu_list.php msgid "Add Proposal" @@ -142,16 +143,16 @@ msgid "Type" msgstr "类别" #: html/addvote.php -msgid "Addition of a TU" -msgstr "添加受信用户" +msgid "Addition of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU" -msgstr "移除受信用户" +msgid "Removal of a Package Maintainer" +msgstr "" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "移除受信用户(无故不活跃)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "" #: html/addvote.php msgid "Amendment of Bylaws" @@ -208,9 +209,10 @@ msgstr "搜索共同维护的软件包" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "欢迎来到 AUR!想了解更多信息,请阅读 %sAUR 用户指南%s 和 %sAUR 受信用户指南%s。" +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "" #: html/home.php #, php-format @@ -224,8 +226,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "记得为您喜欢的软件包投票!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "部分软件包将在 [community] 仓库以二进制包的形式提供。" +msgid "Some packages may be provided as binaries in [extra]." +msgstr "部分软件包将在 [extra] 仓库以二进制包的形式提供。" #: html/home.php msgid "DISCLAIMER" @@ -274,8 +276,8 @@ msgstr "删除请求" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "请求软件包从 AUR 中移除。如果这个包虽然损坏了但是可以被轻易地修好,请不要进行此操作,您应该联系包的维护者或者有必要的情况下发送弃置请求。" +"the maintainer and file orphan request if necessary." +msgstr "" #: html/home.php msgid "Merge Request" @@ -317,10 +319,11 @@ msgstr "邮件列表" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "与 Arch 用户仓库(AUR)或者受信用户结构相关的一般讨论在 %saur-general%s 邮件列表。若是与 AUR web 页面开发相关的讨论,请使用 %saur-dev%s 邮件列表。" +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "" #: html/home.php msgid "Bug Reporting" @@ -331,9 +334,9 @@ msgstr "Bug 报告" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "如果您在 AUR web 页面发现 Bug,请提交到我们的 %sBug 追踪系统%s。追踪系统 %s只能%s 用来报告 AUR web 页面的 Bug。如果想报告打包方面的 Bug,请联系相应的包维护者,或者在相应的软件包页面留下评论。" +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "" #: html/home.php msgid "Package Search" @@ -485,7 +488,7 @@ msgstr "选中的软件包未被弃置,请检查确认复选框。" msgid "" "The selected packages have not been adopted, check the confirmation " "checkbox." -msgstr "" +msgstr "选中的软件包未被接管,请检查确认复选框。" #: html/pkgbase.php lib/pkgreqfuncs.inc.php msgid "Cannot find package to merge votes and comments into." @@ -533,8 +536,8 @@ msgid "Delete" msgstr "删除" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "只有受信用户和开发人员能删除软件包。" +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -574,8 +577,8 @@ msgid "Disown" msgstr "弃置" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "只有受信用户和开发人员能弃置软件包。" +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -591,7 +594,7 @@ msgid "" " package version in the AUR does not match the most recent commit. Flagging " "this package should only be done if the sources moved or changes in the " "PKGBUILD are required because of recent upstream changes." -msgstr "" +msgstr "这似乎是 VCS 软件包。请%s不要%s因为 AUR 中的软件包版本与最新的 commit 不匹配就将其标记为过期。仅当来源移动或由于最新上游更改需要更改 PKGBUILD 时才标记此软件包。" #: html/pkgflag.php #, php-format @@ -664,8 +667,8 @@ msgid "Merge" msgstr "合并" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "只有受信用户和开发人员才能删除软件包。" +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -701,7 +704,7 @@ msgstr "注册" #: html/register.php msgid "Use this form to create an account." -msgstr "使用此表单创建帐号。" +msgstr "使用此表单创建账户。" #: html/tos.php msgid "Terms of Service" @@ -722,8 +725,8 @@ msgid "I accept the terms and conditions above." msgstr "我接受以上条款与条件。" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "受信用户" +msgid "Package Maintainer" +msgstr "" #: html/tu.php msgid "Could not retrieve proposal details." @@ -734,8 +737,8 @@ msgid "Voting is closed for this proposal." msgstr "该提议的投票已被关闭。" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "只有受信用户可以投票。" +msgid "Only Package Maintainers are allowed to vote." +msgstr "" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -854,12 +857,12 @@ msgstr "输入的验证码无效。" #: lib/acctfuncs.inc.php #, php-format msgid "Error trying to create account, %s%s%s." -msgstr "尝试创建帐户 %s%s%s 失败。" +msgstr "尝试创建账户 %s%s%s 失败。" #: lib/acctfuncs.inc.php #, php-format msgid "The account, %s%s%s, has been successfully created." -msgstr "帐户 %s%s%s 创建成功。" +msgstr "账户 %s%s%s 创建成功。" #: lib/acctfuncs.inc.php msgid "A password reset key has been sent to your e-mail address." @@ -867,7 +870,7 @@ msgstr "密码重置密钥已经发送到您的邮箱。" #: lib/acctfuncs.inc.php msgid "Click on the Login link above to use your account." -msgstr "点击上方的登录链接以使用您的帐号。" +msgstr "点击上方的登录链接以使用您的账户。" #: lib/acctfuncs.inc.php #, php-format @@ -877,7 +880,7 @@ msgstr "账户 %s%s%s 没有被修改。" #: lib/acctfuncs.inc.php #, php-format msgid "The account, %s%s%s, has been successfully modified." -msgstr "帐号 %s%s%s 已被成功修改。" +msgstr "账户 %s%s%s 已被成功修改。" #: lib/acctfuncs.inc.php msgid "" @@ -887,11 +890,11 @@ msgstr "登录表单目前对您所使用的 IP 地址禁用,原因可能是 #: lib/acctfuncs.inc.php msgid "Account suspended" -msgstr "帐号被停用" +msgstr "账户被停用" #: aurweb/routers/accounts.py msgid "You do not have permission to suspend accounts." -msgstr "" +msgstr "您没有权限停用此账户。" #: lib/acctfuncs.inc.php #, php-format @@ -980,27 +983,27 @@ msgstr "无法找到软件包的详细信息。" #: aurweb/routers/auth.py msgid "Bad Referer header." -msgstr "" +msgstr "错误的 Referer 消息头。" #: aurweb/routers/packages.py msgid "You did not select any packages to be notified about." -msgstr "" +msgstr "您没有选择要接受通知的软件包。" #: aurweb/routers/packages.py msgid "The selected packages' notifications have been enabled." -msgstr "" +msgstr "选中的软件包的通知已被启用。" #: aurweb/routers/packages.py msgid "You did not select any packages for notification removal." -msgstr "" +msgstr "您没有选择要移除通知的软件包。" #: aurweb/routers/packages.py msgid "A package you selected does not have notifications enabled." -msgstr "" +msgstr "所选中的软件包并没有启用通知。" #: aurweb/routers/packages.py msgid "The selected packages' notifications have been removed." -msgstr "" +msgstr "选中的软件包的通知已被移除。" #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can flag packages." @@ -1040,7 +1043,7 @@ msgstr "您没有选择要删除的软件包。" #: aurweb/routers/packages.py msgid "One of the packages you selected does not exist." -msgstr "" +msgstr "选中的其中一个软件包不存在。" #: lib/pkgbasefuncs.inc.php msgid "The selected packages have been deleted." @@ -1052,7 +1055,7 @@ msgstr "您需要登录后才能接管软件包。" #: aurweb/routers/package.py msgid "You are not allowed to adopt one of the packages you selected." -msgstr "" +msgstr "您不被允许接管选中的其中一个软件包。" #: lib/pkgbasefuncs.inc.php msgid "You must be logged in before you can disown packages." @@ -1060,7 +1063,7 @@ msgstr "您需要登录后才能弃置软件包。" #: aurweb/routers/packages.py msgid "You are not allowed to disown one of the packages you selected." -msgstr "" +msgstr "您不被允许弃置选中的其中一个软件包。" #: lib/pkgbasefuncs.inc.php msgid "You did not select any packages to adopt." @@ -1197,7 +1200,7 @@ msgstr "请求关闭成功。" #: template/account_delete.php #, php-format msgid "You can use this form to permanently delete the AUR account %s." -msgstr "您可以使用这个表单永久删除 AUR 帐号 %s。" +msgstr "您可以使用这个表单永久删除 AUR 账号 %s。" #: template/account_delete.php #, php-format @@ -1216,7 +1219,7 @@ msgstr "用户名" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php msgid "Account Type" -msgstr "帐户类别" +msgstr "账户类别" #: template/account_details.php template/tu_details.php #: template/tu_last_votes_list.php template/tu_list.php @@ -1230,8 +1233,8 @@ msgstr "开发人员" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "受信用户 & 开发者" +msgid "Package Maintainer & Developer" +msgstr "" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1298,7 +1301,7 @@ msgstr "查看这个用户提交的软件包" #: template/account_details.php msgid "Edit this user's account" -msgstr "编辑此用户的帐号" +msgstr "编辑此用户的账户" #: template/account_details.php msgid "List this user's comments" @@ -1307,7 +1310,7 @@ msgstr "显示此用户的评论" #: template/account_edit_form.php #, php-format msgid "Click %shere%s if you want to permanently delete this account." -msgstr "如果你想永久删除这个帐号,请点击 %s这里%s。" +msgstr "如果你想永久删除这个账户,请点击 %s这里%s。" #: template/account_edit_form.php #, php-format @@ -1333,13 +1336,9 @@ msgstr "您的用户名称将用于您的登录。这是公开的,即使您的 msgid "Normal user" msgstr "普通用户" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "受信用户" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" -msgstr "帐户被暂停" +msgstr "账户被停用" #: template/account_edit_form.php msgid "Inactive" @@ -1370,7 +1369,7 @@ msgstr "备用邮件地址" msgid "" "Optionally provide a secondary email address that can be used to restore " "your account in case you lose access to your primary email address." -msgstr "选择性的提供的备用的邮件地址。该邮件地址将在你的主要邮件地址不可用时用于恢复你的帐号。" +msgstr "选择性的提供的备用的邮件地址。该邮件地址将在你的主要邮件地址不可用时用于恢复你的账户。" #: template/account_edit_form.php msgid "" @@ -1409,6 +1408,15 @@ msgid "" " the Arch User Repository." msgstr "仅当你想向 AUR 提交软件包时才需要填写以下信息。" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "SSH 公钥" @@ -1466,7 +1474,7 @@ msgstr "没有结果符合您的搜索条件。" #: template/account_search_results.php msgid "Edit Account" -msgstr "编辑帐户" +msgstr "编辑账户" #: template/account_search_results.php msgid "Suspended" @@ -1528,7 +1536,7 @@ msgstr "版权所有 %s 2004-%d aurweb 开发组。" #: template/header.php msgid " My Account" -msgstr " 我的帐户" +msgstr " 我的账户" #: template/pkgbase_actions.php msgid "Package Actions" @@ -1831,26 +1839,26 @@ msgstr "合并到" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "通过提交删除请求,您请求受信用户进行包基础的删除。这种请求应当被用于重复,软件被上游放弃,非法或损坏且无法修复的软件包。" +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "通过提交合并请求,您请求受信用户进行包基础的删除,并转移其投票和评论到另一包基础。合并一个软件包不会影响对应的 Git 项目,因此您需要自己更新目标软件包的 Git 项目。" +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "通过提交弃置请求,您请求受信用户进行包基础的弃置。请仅在软件包需要维护者操作,您之前已经尝试联系过维护者但没有回应时提交请求。" +msgstr "" #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -2099,8 +2107,8 @@ msgid "Registered Users" msgstr "注册用户" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "受信用户" +msgid "Package Maintainers" +msgstr "" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2285,8 +2293,8 @@ msgstr "用户 {user} [1] 删除了软件包 {pkgbase} [2]。\n\n您将不再收 #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" -msgstr "受信用户投票提醒:提案 {id}" +msgid "Package Maintainer Vote Reminder: Proposal {id}" +msgstr "" #: scripts/notify.py #, python-brace-format @@ -2297,45 +2305,77 @@ msgstr "请记得为提案 {id} [1] 投票,投票时段将于48小时内结束 #: aurweb/routers/accounts.py msgid "Invalid account type provided." -msgstr "" +msgstr "提供的账户类别无效。" #: aurweb/routers/accounts.py msgid "You do not have permission to change account types." -msgstr "" +msgstr "您没有权限更改账户类别。" #: aurweb/routers/accounts.py msgid "You do not have permission to change this user's account type to %s." -msgstr "" +msgstr "您没有权限将此用户的账户类别更改为%s。" #: aurweb/packages/requests.py msgid "No due existing orphan requests to accept for %s." -msgstr "" +msgstr "没有为 %s 接受的现有孤立请求。" #: aurweb/asgi.py msgid "Internal Server Error" -msgstr "" +msgstr "内部服务器错误" #: templates/errors/500.html msgid "A fatal error has occurred." -msgstr "" +msgstr "发生了严重的错误。" #: templates/errors/500.html msgid "" "Details have been logged and will be reviewed by the postmaster posthaste. " "We apologize for any inconvenience this may have caused." -msgstr "" +msgstr "详细信息已被记录,并会交由 Postmaster 尽快调查。对您造成的不便,我们深感抱歉。" #: aurweb/scripts/notify.py msgid "AUR Server Error" -msgstr "" +msgstr "AUR 服务器错误" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "相关软件包请求关闭评论…" #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." +msgstr "此操作将关闭任何有关的未处理的软件包请求。若省略%s评论%s,将会自动生成关闭评论。" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "" + +#: templates/requests.html +msgid "Package name" +msgstr "" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." msgstr "" diff --git a/po/zh_TW.po b/po/zh_TW.po index e7399a19..40521236 100644 --- a/po/zh_TW.po +++ b/po/zh_TW.po @@ -4,16 +4,17 @@ # # Translators: # pan93412 , 2018 +# Cycatz , 2022 # 黃柏諺 , 2014-2017 -# 黃柏諺 , 2020-2022 +# 黃柏諺 , 2020-2023 msgid "" msgstr "" "Project-Id-Version: aurweb\n" -"Report-Msgid-Bugs-To: https://bugs.archlinux.org/index.php?project=2\n" +"Report-Msgid-Bugs-To: https://gitlab.archlinux.org/archlinux/aurweb/-/issues\n" "POT-Creation-Date: 2020-01-31 09:29+0100\n" -"PO-Revision-Date: 2022-01-18 17:18+0000\n" -"Last-Translator: Kevin Morris \n" -"Language-Team: Chinese (Taiwan) (http://www.transifex.com/lfleischer/aurweb/language/zh_TW/)\n" +"PO-Revision-Date: 2011-04-10 13:21+0000\n" +"Last-Translator: 黃柏諺 , 2020-2023\n" +"Language-Team: Chinese (Taiwan) (http://app.transifex.com/lfleischer/aurweb/language/zh_TW/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -134,16 +135,16 @@ msgid "Type" msgstr "類型" #: html/addvote.php -msgid "Addition of a TU" -msgstr "添加受信使用者" +msgid "Addition of a Package Maintainer" +msgstr "新增軟體包維護者" #: html/addvote.php -msgid "Removal of a TU" -msgstr "移除受信使用者" +msgid "Removal of a Package Maintainer" +msgstr "移除軟體包維護者" #: html/addvote.php -msgid "Removal of a TU (undeclared inactivity)" -msgstr "移除受信使用者(無故不活躍)" +msgid "Removal of a Package Maintainer (undeclared inactivity)" +msgstr "移除軟體包維護者(無故不活躍)" #: html/addvote.php msgid "Amendment of Bylaws" @@ -200,9 +201,10 @@ msgstr "搜尋我共同維護的套件" #: html/home.php #, php-format msgid "" -"Welcome to the AUR! Please read the %sAUR User Guidelines%s and %sAUR TU " -"Guidelines%s for more information." -msgstr "歡迎來到 AUR!請閱讀 %sAUR 使用者指導方針%s 和 %sAUR 受信使用者指導方針%s 以獲取更多的詳細資訊。" +"Welcome to the AUR! Please read the %sAUR User Guidelines%s for more " +"information and the %sAUR Submission Guidelines%s if you want to contribute " +"a PKGBUILD." +msgstr "歡迎使用 AUR!請閱讀 %sAUR 使用者指南%s以取得更多資訊,若您想要貢獻 PKGBUILD,也請閱讀 %sAUR 遞交指南%s。" #: html/home.php #, php-format @@ -216,8 +218,8 @@ msgid "Remember to vote for your favourite packages!" msgstr "記得投一票給您喜愛的套件!" #: html/home.php -msgid "Some packages may be provided as binaries in [community]." -msgstr "某些套件可能會在 [community] 提供二進位檔案。" +msgid "Some packages may be provided as binaries in [extra]." +msgstr "某些套件可能會在 [extra] 提供二進位檔案。" #: html/home.php msgid "DISCLAIMER" @@ -266,8 +268,8 @@ msgstr "刪除請求" msgid "" "Request a package to be removed from the Arch User Repository. Please do not" " use this if a package is broken and can be fixed easily. Instead, contact " -"the package maintainer and file orphan request if necessary." -msgstr "請求套件從 Arch 使用者套件庫中移除。如果套件損毀但可以很容易的被修復,請不要使用這個請求。您應該聯絡套件維護者,並在必要時提出棄置請求。" +"the maintainer and file orphan request if necessary." +msgstr "請求軟體包從 Arch 使用者軟體庫中移除。如果軟體包損毀但可以很容易的被修復,請不要使用這個請求。您應該聯絡維護者,並在必要時提出棄置請求。" #: html/home.php msgid "Merge Request" @@ -309,10 +311,11 @@ msgstr "討論" #: html/home.php #, php-format msgid "" -"General discussion regarding the Arch User Repository (AUR) and Trusted User" -" structure takes place on %saur-general%s. For discussion relating to the " -"development of the AUR web interface, use the %saur-dev%s mailing list." -msgstr "與 Arch 使用者套件庫(AUR)及受信使用者結構相關的一般性討論請見 %saur-general%s 。討論關於 AUR 網頁介面的開發,請使用 %saur-dev%s 郵件列表。" +"General discussion regarding the Arch User Repository (AUR) and Package " +"Maintainer structure takes place on %saur-general%s. For discussion relating" +" to the development of the AUR web interface, use the %saur-dev%s mailing " +"list." +msgstr "與 Arch 使用者軟體庫 (AUR) 與軟體包維護者結構相關的一般性討論請見 %saur-general%s 。討論關於 AUR 網頁介面的開發,請使用 %saur-dev%s 郵件列表。" #: html/home.php msgid "Bug Reporting" @@ -323,9 +326,9 @@ msgstr "臭蟲回報" msgid "" "If you find a bug in the AUR web interface, please fill out a bug report on " "our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface" -" %sonly%s. To report packaging bugs contact the package maintainer or leave " -"a comment on the appropriate package page." -msgstr "如果您在 AUR 網頁介面中發現了臭蟲,請在我們的 %s臭蟲追蹤系統%s 中回報。請%s只%s回報 AUR 網頁介面本身的臭蟲。要回報打包臭蟲,請連絡套件的維護者或是在對應的套件頁面中留下評論。" +" %sonly%s. To report packaging bugs contact the maintainer or leave a " +"comment on the appropriate package page." +msgstr "如果您在 AUR 網頁介面中發現了臭蟲,請在我們的 %s臭蟲追蹤系統%s 中回報。請%s只%s回報 AUR 網頁介面本身的臭蟲。要回報打包臭蟲,請連絡維護者或是在對應的軟體包頁面中留下評論。" #: html/home.php msgid "Package Search" @@ -525,8 +528,8 @@ msgid "Delete" msgstr "刪除" #: html/pkgdel.php -msgid "Only Trusted Users and Developers can delete packages." -msgstr "只有受信使用者和開發者可以刪除套件。" +msgid "Only Package Maintainers and Developers can delete packages." +msgstr "僅軟體包維護者與開發者可以刪除軟體包。" #: html/pkgdisown.php template/pkgbase_actions.php msgid "Disown Package" @@ -566,8 +569,8 @@ msgid "Disown" msgstr "棄置" #: html/pkgdisown.php -msgid "Only Trusted Users and Developers can disown packages." -msgstr "只有受信使用者和開發者可以棄置套件。" +msgid "Only Package Maintainers and Developers can disown packages." +msgstr "僅軟體包維護者與開發者可以棄置軟體包。" #: html/pkgflagcomment.php msgid "Flag Comment" @@ -656,8 +659,8 @@ msgid "Merge" msgstr "合併" #: html/pkgmerge.php -msgid "Only Trusted Users and Developers can merge packages." -msgstr "只有受信使用者和開發者可以合併套件。" +msgid "Only Package Maintainers and Developers can merge packages." +msgstr "僅軟體包維護者與開發者可以合併軟體包。" #: html/pkgreq.php template/pkgbase_actions.php template/pkgreq_form.php msgid "Submit Request" @@ -714,8 +717,8 @@ msgid "I accept the terms and conditions above." msgstr "我接受上述條款與條件。" #: html/tu.php template/account_details.php template/header.php -msgid "Trusted User" -msgstr "受信使用者" +msgid "Package Maintainer" +msgstr "軟體包維護者" #: html/tu.php msgid "Could not retrieve proposal details." @@ -726,8 +729,8 @@ msgid "Voting is closed for this proposal." msgstr "這個建議的投票已被關閉。" #: html/tu.php -msgid "Only Trusted Users are allowed to vote." -msgstr "只有受信使用者可以投票。" +msgid "Only Package Maintainers are allowed to vote." +msgstr "僅軟體包維護者可以投票。" #: html/tu.php msgid "You cannot vote in an proposal about you." @@ -1222,8 +1225,8 @@ msgstr "開發者" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php -msgid "Trusted User & Developer" -msgstr "受信使用者 & 開發者" +msgid "Package Maintainer & Developer" +msgstr "軟體包維護者與開發者" #: template/account_details.php template/account_edit_form.php #: template/search_accounts_form.php @@ -1325,10 +1328,6 @@ msgstr "您的使用者名稱是您要用於登入的名稱。它是公開的, msgid "Normal user" msgstr "一般使用者" -#: template/account_edit_form.php template/search_accounts_form.php -msgid "Trusted user" -msgstr "受信使用者" - #: template/account_edit_form.php template/search_accounts_form.php msgid "Account Suspended" msgstr "帳號被暫停" @@ -1401,6 +1400,15 @@ msgid "" " the Arch User Repository." msgstr "以下的資訊僅在您想要遞交套件到 Arch 使用者套件庫時是必須的。" +#: templates/partials/account_form.html +msgid "" +"Specify multiple SSH Keys separated by new line, empty lines are ignored." +msgstr "指定多個 SSH 金鑰,以換行符號分隔,將會忽略空行。" + +#: templates/partials/account_form.html +msgid "Hide deleted comments" +msgstr "隱藏已刪除的評論" + #: template/account_edit_form.php msgid "SSH Public Key" msgstr "SSH 公開金鑰" @@ -1823,26 +1831,26 @@ msgstr "合併到" #: template/pkgreq_form.php msgid "" -"By submitting a deletion request, you ask a Trusted User to delete the " -"package base. This type of request should be used for duplicates, software " +"By submitting a deletion request, you ask a Package Maintainer to delete the" +" package base. This type of request should be used for duplicates, software " "abandoned by upstream, as well as illegal and irreparably broken packages." -msgstr "透過遞交刪除請求,您會請求受信使用者刪除套件基礎。這個類型的請求應該用於重複、被上游放棄的軟體,以及違法與無法修復的損壞套件。" +msgstr "透過遞交刪除請求,您會請求軟體包維護者刪除套件基礎。這個類型的請求應該用於重複、被上游放棄的軟體,以及違法與無法修復的損壞軟體包。" #: template/pkgreq_form.php msgid "" -"By submitting a merge request, you ask a Trusted User to delete the package " -"base and transfer its votes and comments to another package base. Merging a " -"package does not affect the corresponding Git repositories. Make sure you " -"update the Git history of the target package yourself." -msgstr "透過遞交合併請求,您會請求受信使用者刪除套件基礎並轉移其投票數到其他的套件基礎。合併一個套件不會影響相對應的 Git 倉庫。確保您可以自己更新目標套件的 Git 歷史。" +"By submitting a merge request, you ask a Package Maintainer to delete the " +"package base and transfer its votes and comments to another package base. " +"Merging a package does not affect the corresponding Git repositories. Make " +"sure you update the Git history of the target package yourself." +msgstr "透過遞交合併請求,您會請求軟體包維護者刪除軟體包基礎並轉移其投票數到其他的軟體包基礎。合併一個軟體包不會影響相對應的 Git 倉庫。確保您可以自己更新目標軟體包的 Git 歷史。" #: template/pkgreq_form.php msgid "" -"By submitting an orphan request, you ask a Trusted User to disown the " +"By submitting an orphan request, you ask a Package Maintainer to disown the " "package base. Please only do this if the package needs maintainer action, " "the maintainer is MIA and you already tried to contact the maintainer " "previously." -msgstr "透過遞交棄置請求,您會請求受信使用者棄置套件基礎。請僅在該套件需要有維護者動作、維護者突然消失且您先前已經連絡過維護者時做此動作。" +msgstr "透過遞交棄置請求,您會請求軟體包維護者棄置軟體包基礎。請僅在該軟體包需要有維護者動作、維護者突然消失且您先前已經連絡過維護者時做此動作。" #: template/pkgreq_results.php msgid "No requests matched your search criteria." @@ -1990,7 +1998,7 @@ msgstr "每頁顯示" #: template/pkg_search_form.php template/pkg_search_results.php msgid "Go" -msgstr "到" +msgstr "搜尋" #: template/pkg_search_form.php msgid "Orphans" @@ -2091,8 +2099,8 @@ msgid "Registered Users" msgstr "已註冊的使用者" #: template/stats/general_stats_table.php -msgid "Trusted Users" -msgstr "受信使用者" +msgid "Package Maintainers" +msgstr "軟體包維護者" #: template/stats/updates_table.php msgid "Recent Updates" @@ -2277,8 +2285,8 @@ msgstr "{user} [1] 已經刪除 {pkgbase} [2]。\n\n您將再也不會收到此 #: scripts/notify.py #, python-brace-format -msgid "TU Vote Reminder: Proposal {id}" -msgstr "TU 投票提醒:編號為 {id} 的建議" +msgid "Package Maintainer Vote Reminder: Proposal {id}" +msgstr "軟體包維護者投票提醒:編號為 {id} 的建議" #: scripts/notify.py #, python-brace-format @@ -2324,10 +2332,42 @@ msgstr "AUR 伺服器錯誤" #: templates/pkgbase/merge.html templates/packages/delete.html #: templates/packages/disown.html msgid "Related package request closure comments..." -msgstr "" +msgstr "相關軟體包請求關閉留言……" #: templates/pkgbase/merge.html templates/packages/delete.html msgid "" "This action will close any pending package requests related to it. If " "%sComments%s are omitted, a closure comment will be autogenerated." -msgstr "" +msgstr "此動作將會關閉任何關於此的擱置中軟體包請求。若省略%s留言%s,將會自動產生關閉留言。" + +#: templates/partials/tu/proposal/details.html +msgid "assigned" +msgstr "分配" + +#: templaets/partials/packages/package_metadata.html +msgid "Show %d more" +msgstr "再顯示 %d 個" + +#: templates/partials/packages/package_metadata.html +msgid "dependencies" +msgstr "依賴關係" + +#: aurweb/routers/accounts.py +msgid "The account has not been deleted, check the confirmation checkbox." +msgstr "帳號未被刪除,請檢查確認的核取方塊。" + +#: templates/partials/packages/comment_form.html +msgid "Cancel" +msgstr "取消" + +#: templates/requests.html +msgid "Package name" +msgstr "軟體包名稱" + +#: templates/partials/account_form.html +msgid "" +"Note that if you hide your email address, it'll end up on the BCC list for " +"any request notifications. In case someone replies to these notifications, " +"you won't receive an email. However, replies are typically sent to the " +"mailing-list and would then be visible in the archive." +msgstr "請注意,如果您隱藏您的電子郵件地址,它最終會出現在任何要求通知的密件副本清單中。如果有人回覆這些通知,您將不會收到電子郵件。但是,回覆通常會傳送到郵遞論壇,然後在存檔中可見。" diff --git a/poetry.lock b/poetry.lock index c9d0b38a..72638b4e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,101 +1,145 @@ +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. + [[package]] name = "aiofiles" -version = "0.7.0" +version = "24.1.0" description = "File support for asyncio." -category = "main" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.8" +files = [ + {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, + {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, +] [[package]] name = "alembic" -version = "1.7.6" +version = "1.13.2" description = "A database migration tool for SQLAlchemy." -category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +files = [ + {file = "alembic-1.13.2-py3-none-any.whl", hash = "sha256:6b8733129a6224a9a711e17c99b08462dbf7cc9670ba8f2e2ae9af860ceb1953"}, + {file = "alembic-1.13.2.tar.gz", hash = "sha256:1ff0ae32975f4fd96028c39ed9bb3c867fe3af956bd7bb37343b54c9fe7445ef"}, +] [package.dependencies] Mako = "*" SQLAlchemy = ">=1.3.0" +typing-extensions = ">=4" [package.extras] -tz = ["python-dateutil"] +tz = ["backports.zoneinfo"] + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] [[package]] name = "anyio" -version = "3.5.0" +version = "4.4.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "main" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.8" +files = [ + {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, + {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, +] [package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] -doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] -test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=6.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] -trio = ["trio (>=0.16)"] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] [[package]] name = "asgiref" -version = "3.5.0" +version = "3.8.1" description = "ASGI specs, helper code, and adapters" -category = "main" +optional = false +python-versions = ">=3.8" +files = [ + {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, + {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} + +[package.extras] +tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] + +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.7" - -[package.extras] -tests = ["pytest", "pytest-asyncio", "mypy (>=0.800)"] - -[[package]] -name = "atomicwrites" -version = "1.4.0" -description = "Atomic file writes." -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "attrs" -version = "21.4.0" -description = "Classes Without Boilerplate" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] [[package]] name = "authlib" -version = "0.15.5" -description = "The ultimate Python library in building OAuth and OpenID Connect servers." -category = "main" +version = "1.3.1" +description = "The ultimate Python library in building OAuth and OpenID Connect servers and clients." optional = false -python-versions = "*" +python-versions = ">=3.8" +files = [ + {file = "Authlib-1.3.1-py2.py3-none-any.whl", hash = "sha256:d35800b973099bbadc49b42b256ecb80041ad56b7fe1216a362c7943c088f377"}, + {file = "authlib-1.3.1.tar.gz", hash = "sha256:7ae843f03c06c5c0debd63c9db91f9fda64fa62a42a77419fa15fbb7e7a58917"}, +] [package.dependencies] cryptography = "*" -[package.extras] -client = ["requests"] - [[package]] name = "bcrypt" -version = "3.2.0" +version = "4.2.0" description = "Modern password hashing for your software and your servers" -category = "main" optional = false -python-versions = ">=3.6" - -[package.dependencies] -cffi = ">=1.1" -six = ">=1.4.1" +python-versions = ">=3.7" +files = [ + {file = "bcrypt-4.2.0-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:096a15d26ed6ce37a14c1ac1e48119660f21b24cba457f160a4b830f3fe6b5cb"}, + {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c02d944ca89d9b1922ceb8a46460dd17df1ba37ab66feac4870f6862a1533c00"}, + {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d84cf6d877918620b687b8fd1bf7781d11e8a0998f576c7aa939776b512b98d"}, + {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1bb429fedbe0249465cdd85a58e8376f31bb315e484f16e68ca4c786dcc04291"}, + {file = "bcrypt-4.2.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:655ea221910bcac76ea08aaa76df427ef8625f92e55a8ee44fbf7753dbabb328"}, + {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:1ee38e858bf5d0287c39b7a1fc59eec64bbf880c7d504d3a06a96c16e14058e7"}, + {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:0da52759f7f30e83f1e30a888d9163a81353ef224d82dc58eb5bb52efcabc399"}, + {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3698393a1b1f1fd5714524193849d0c6d524d33523acca37cd28f02899285060"}, + {file = "bcrypt-4.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:762a2c5fb35f89606a9fde5e51392dad0cd1ab7ae64149a8b935fe8d79dd5ed7"}, + {file = "bcrypt-4.2.0-cp37-abi3-win32.whl", hash = "sha256:5a1e8aa9b28ae28020a3ac4b053117fb51c57a010b9f969603ed885f23841458"}, + {file = "bcrypt-4.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:8f6ede91359e5df88d1f5c1ef47428a4420136f3ce97763e31b86dd8280fbdf5"}, + {file = "bcrypt-4.2.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:c52aac18ea1f4a4f65963ea4f9530c306b56ccd0c6f8c8da0c06976e34a6e841"}, + {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bbbfb2734f0e4f37c5136130405332640a1e46e6b23e000eeff2ba8d005da68"}, + {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3413bd60460f76097ee2e0a493ccebe4a7601918219c02f503984f0a7ee0aebe"}, + {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8d7bb9c42801035e61c109c345a28ed7e84426ae4865511eb82e913df18f58c2"}, + {file = "bcrypt-4.2.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3d3a6d28cb2305b43feac298774b997e372e56c7c7afd90a12b3dc49b189151c"}, + {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:9c1c4ad86351339c5f320ca372dfba6cb6beb25e8efc659bedd918d921956bae"}, + {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:27fe0f57bb5573104b5a6de5e4153c60814c711b29364c10a75a54bb6d7ff48d"}, + {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8ac68872c82f1add6a20bd489870c71b00ebacd2e9134a8aa3f98a0052ab4b0e"}, + {file = "bcrypt-4.2.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cb2a8ec2bc07d3553ccebf0746bbf3d19426d1c6d1adbd4fa48925f66af7b9e8"}, + {file = "bcrypt-4.2.0-cp39-abi3-win32.whl", hash = "sha256:77800b7147c9dc905db1cba26abe31e504d8247ac73580b4aa179f98e6608f34"}, + {file = "bcrypt-4.2.0-cp39-abi3-win_amd64.whl", hash = "sha256:61ed14326ee023917ecd093ee6ef422a72f3aec6f07e21ea5f10622b735538a9"}, + {file = "bcrypt-4.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:39e1d30c7233cfc54f5c3f2c825156fe044efdd3e0b9d309512cc514a263ec2a"}, + {file = "bcrypt-4.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f4f4acf526fcd1c34e7ce851147deedd4e26e6402369304220250598b26448db"}, + {file = "bcrypt-4.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:1ff39b78a52cf03fdf902635e4c81e544714861ba3f0efc56558979dd4f09170"}, + {file = "bcrypt-4.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:373db9abe198e8e2c70d12b479464e0d5092cc122b20ec504097b5f2297ed184"}, + {file = "bcrypt-4.2.0.tar.gz", hash = "sha256:cf69eaf5185fd58f268f805b505ce31f9b9fc2d64b376642164e9244540c1221"}, +] [package.extras] tests = ["pytest (>=3.2.1,!=3.3.0)"] @@ -103,192 +147,504 @@ typecheck = ["mypy"] [[package]] name = "bleach" -version = "4.1.0" +version = "6.1.0" description = "An easy safelist-based HTML-sanitizing tool." -category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +files = [ + {file = "bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6"}, + {file = "bleach-6.1.0.tar.gz", hash = "sha256:0a31f1837963c41d46bbf1331b8778e1308ea0791db03cc4e7357b97cf42a8fe"}, +] [package.dependencies] -packaging = "*" six = ">=1.9.0" webencodings = "*" +[package.extras] +css = ["tinycss2 (>=1.1.0,<1.3)"] + [[package]] name = "certifi" -version = "2021.10.8" +version = "2024.7.4" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false -python-versions = "*" +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, +] [[package]] name = "cffi" -version = "1.15.0" +version = "1.17.0" description = "Foreign Function Interface for Python calling C code." -category = "main" optional = false -python-versions = "*" +python-versions = ">=3.8" +files = [ + {file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"}, + {file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"}, + {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"}, + {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"}, + {file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"}, + {file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"}, + {file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"}, + {file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"}, + {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"}, + {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"}, + {file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"}, + {file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"}, + {file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"}, + {file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"}, + {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"}, + {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"}, + {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"}, + {file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"}, + {file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"}, + {file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"}, + {file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"}, + {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"}, + {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"}, + {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"}, + {file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"}, + {file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"}, + {file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"}, + {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"}, + {file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"}, + {file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"}, + {file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"}, + {file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"}, + {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"}, + {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"}, + {file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"}, + {file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"}, + {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"}, +] [package.dependencies] pycparser = "*" [[package]] name = "charset-normalizer" -version = "2.0.11" +version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false -python-versions = ">=3.5.0" - -[package.extras] -unicode_backport = ["unicodedata2"] +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] [[package]] name = "click" -version = "8.0.3" +version = "8.1.7" description = "Composable command line interface toolkit" -category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "colorama" -version = "0.4.4" +version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] [[package]] name = "coverage" -version = "6.3.1" +version = "7.6.1" description = "Code coverage measurement for Python" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, + {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, + {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, + {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, + {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, +] [package.dependencies] -tomli = {version = "*", optional = true, markers = "extra == \"toml\""} +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli"] [[package]] name = "cryptography" -version = "36.0.1" +version = "43.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"}, + {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"}, + {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"}, + {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"}, + {file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"}, + {file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"}, + {file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"}, + {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"}, + {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"}, + {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"}, + {file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"}, + {file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"}, + {file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"}, + {file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"}, + {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"}, +] [package.dependencies] -cffi = ">=1.12" +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] -pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] -sdist = ["setuptools_rust (>=0.11.4)"] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "deprecated" +version = "1.2.14" +description = "Python @deprecated decorator to deprecate old python classes, functions or methods." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"}, + {file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"}, +] + +[package.dependencies] +wrapt = ">=1.10,<2" + +[package.extras] +dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] [[package]] name = "dnspython" -version = "2.2.0" +version = "2.6.1" description = "DNS toolkit" -category = "main" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.8" +files = [ + {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, + {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, +] [package.extras] -dnssec = ["cryptography (>=2.6,<37.0)"] -curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"] -doh = ["h2 (>=4.1.0)", "httpx (>=0.21.1)", "requests (>=2.23.0,<3.0.0)", "requests-toolbelt (>=0.9.1,<0.10.0)"] -idna = ["idna (>=2.1,<4.0)"] -trio = ["trio (>=0.14,<0.20)"] -wmi = ["wmi (>=1.5.1,<2.0.0)"] - -[[package]] -name = "dunamai" -version = "1.8.0" -description = "Dynamic version generation" -category = "main" -optional = false -python-versions = ">=3.5,<4.0" - -[package.dependencies] -packaging = ">=20.9" +dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "sphinx (>=7.2.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] +dnssec = ["cryptography (>=41)"] +doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] +doq = ["aioquic (>=0.9.25)"] +idna = ["idna (>=3.6)"] +trio = ["trio (>=0.23)"] +wmi = ["wmi (>=1.5.1)"] [[package]] name = "email-validator" -version = "1.1.3" -description = "A robust email syntax and deliverability validation library for Python 2.x/3.x." -category = "main" +version = "2.2.0" +description = "A robust email address syntax and deliverability validation library." optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.8" +files = [ + {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, + {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, +] [package.dependencies] -dnspython = ">=1.15.0" +dnspython = ">=2.0.0" idna = ">=2.0.0" [[package]] -name = "execnet" -version = "1.9.0" -description = "execnet: rapid multi-Python deployment" -category = "main" +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] [package.extras] -testing = ["pre-commit"] +test = ["pytest (>=6)"] + +[[package]] +name = "execnet" +version = "2.1.1" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.8" +files = [ + {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, + {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] [[package]] name = "fakeredis" -version = "1.7.0" -description = "Fake implementation of redis API for testing purposes." -category = "main" +version = "2.23.5" +description = "Python implementation of redis API, can be used for testing purposes." optional = false -python-versions = ">=3.5" +python-versions = "<4.0,>=3.7" +files = [ + {file = "fakeredis-2.23.5-py3-none-any.whl", hash = "sha256:4d85b1b6b3a80cbbb3a8967f8686f7bf6ddf5bd7cd5ac7ac90b3561d8c3a7ddb"}, + {file = "fakeredis-2.23.5.tar.gz", hash = "sha256:edffc79fdce0f1d83cbb20b52694a9cba4a5fe5beb627c11722a42aa0fa44f52"}, +] [package.dependencies] -packaging = "*" -redis = "<4.1.0" -six = ">=1.12" -sortedcontainers = "*" +redis = ">=4" +sortedcontainers = ">=2,<3" +typing_extensions = {version = ">=4.7,<5.0", markers = "python_version < \"3.11\""} [package.extras] -aioredis = ["aioredis"] -lua = ["lupa"] +bf = ["pyprobables (>=0.6,<0.7)"] +cf = ["pyprobables (>=0.6,<0.7)"] +json = ["jsonpath-ng (>=1.6,<2.0)"] +lua = ["lupa (>=2.1,<3.0)"] +probabilistic = ["pyprobables (>=0.6,<0.7)"] [[package]] name = "fastapi" -version = "0.71.0" +version = "0.112.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -category = "main" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.112.1-py3-none-any.whl", hash = "sha256:bcbd45817fc2a1cd5da09af66815b84ec0d3d634eb173d1ab468ae3103e183e4"}, + {file = "fastapi-0.112.1.tar.gz", hash = "sha256:b2537146f8c23389a7faa8b03d0bd38d4986e6983874557d95eed2acc46448ef"}, +] [package.dependencies] -pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" -starlette = "0.17.1" +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.37.2,<0.39.0" +typing-extensions = ">=4.8.0" [package.extras] -all = ["requests (>=2.24.0,<3.0.0)", "jinja2 (>=2.11.2,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "itsdangerous (>=1.1.0,<3.0.0)", "pyyaml (>=5.3.1,<6.0.0)", "ujson (>=4.0.1,<5.0.0)", "orjson (>=3.2.1,<4.0.0)", "email_validator (>=1.1.1,<2.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] -dev = ["python-jose[cryptography] (>=3.3.0,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "autoflake (>=1.4.0,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "uvicorn[standard] (>=0.12.0,<0.16.0)"] -doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "typer-cli (>=0.0.12,<0.0.13)", "pyyaml (>=5.3.1,<6.0.0)"] -test = ["pytest (>=6.2.4,<7.0.0)", "pytest-cov (>=2.12.0,<4.0.0)", "mypy (==0.910)", "flake8 (>=3.8.3,<4.0.0)", "black (==21.9b0)", "isort (>=5.0.6,<6.0.0)", "requests (>=2.24.0,<3.0.0)", "httpx (>=0.14.0,<0.19.0)", "email_validator (>=1.1.1,<2.0.0)", "sqlalchemy (>=1.3.18,<1.5.0)", "peewee (>=3.13.3,<4.0.0)", "databases[sqlite] (>=0.3.2,<0.6.0)", "orjson (>=3.2.1,<4.0.0)", "ujson (>=4.0.1,<5.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "flask (>=1.1.2,<3.0.0)", "anyio[trio] (>=3.2.1,<4.0.0)", "types-ujson (==0.1.1)", "types-orjson (==3.6.0)", "types-dataclasses (==0.1.7)"] +all = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "feedgen" -version = "0.9.0" +version = "1.0.0" description = "Feed Generator (ATOM, RSS, Podcasts)" -category = "main" optional = false python-versions = "*" +files = [ + {file = "feedgen-1.0.0.tar.gz", hash = "sha256:d9bd51c3b5e956a2a52998c3708c4d2c729f2fcc311188e1e5d3b9726393546a"}, +] [package.dependencies] lxml = "*" @@ -296,69 +652,165 @@ python-dateutil = "*" [[package]] name = "filelock" -version = "3.4.2" +version = "3.15.4" description = "A platform independent file lock." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, + {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, +] [package.extras] -docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] -testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] +typing = ["typing-extensions (>=4.8)"] [[package]] -name = "flake8" -version = "4.0.1" -description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" +name = "googleapis-common-protos" +version = "1.63.2" +description = "Common protobufs used in Google APIs" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "googleapis-common-protos-1.63.2.tar.gz", hash = "sha256:27c5abdffc4911f28101e635de1533fb4cfd2c37fbaa9174587c799fac90aa87"}, + {file = "googleapis_common_protos-1.63.2-py2.py3-none-any.whl", hash = "sha256:27a2499c7e8aff199665b22741997e485eccc8645aa9176c7c988e6fae507945"}, +] [package.dependencies] -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.8.0,<2.9.0" -pyflakes = ">=2.4.0,<2.5.0" +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" + +[package.extras] +grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] [[package]] name = "greenlet" -version = "1.1.2" +version = "3.1.1" description = "Lightweight in-process concurrent programming" -category = "main" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" +python-versions = ">=3.7" +files = [ + {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"}, + {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"}, + {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"}, + {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"}, + {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"}, + {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"}, + {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"}, + {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de"}, + {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa"}, + {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af"}, + {file = "greenlet-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798"}, + {file = "greenlet-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef"}, + {file = "greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1"}, + {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd"}, + {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7"}, + {file = "greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef"}, + {file = "greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d"}, + {file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"}, + {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"}, + {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"}, + {file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"}, + {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"}, + {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"}, +] [package.extras] -docs = ["sphinx"] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] [[package]] name = "gunicorn" -version = "20.1.0" +version = "22.0.0" description = "WSGI HTTP Server for UNIX" -category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" +files = [ + {file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"}, + {file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"}, +] + +[package.dependencies] +packaging = "*" [package.extras] -eventlet = ["eventlet (>=0.24.1)"] +eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] gevent = ["gevent (>=1.4.0)"] setproctitle = ["setproctitle"] +testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] tornado = ["tornado (>=0.2)"] [[package]] name = "h11" -version = "0.12.0" +version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] [[package]] name = "h2" version = "4.1.0" description = "HTTP/2 State-Machine based protocol implementation" -category = "main" optional = false python-versions = ">=3.6.1" +files = [ + {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, + {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, +] [package.dependencies] hpack = ">=4.0,<5" @@ -368,136 +820,158 @@ hyperframe = ">=6.0,<7" name = "hpack" version = "4.0.0" description = "Pure-Python HPACK header compression" -category = "main" optional = false python-versions = ">=3.6.1" +files = [ + {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, + {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, +] [[package]] name = "httpcore" -version = "0.13.7" +version = "1.0.5" description = "A minimal low-level HTTP client." -category = "main" optional = false -python-versions = ">=3.6" - -[package.dependencies] -anyio = ">=3.0.0,<4.0.0" -h11 = ">=0.11,<0.13" -sniffio = ">=1.0.0,<2.0.0" - -[package.extras] -http2 = ["h2 (>=3,<5)"] - -[[package]] -name = "httpx" -version = "0.20.0" -description = "The next generation HTTP client." -category = "main" -optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, +] [package.dependencies] certifi = "*" -charset-normalizer = "*" -httpcore = ">=0.13.3,<0.14.0" -rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.26.0)"] + +[[package]] +name = "httpx" +version = "0.27.0" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, + {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" sniffio = "*" [package.extras] -brotli = ["brotlicffi", "brotli"] -cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10.0.0,<11.0.0)", "pygments (>=2.0.0,<3.0.0)"] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] [[package]] name = "hypercorn" -version = "0.11.2" -description = "A ASGI Server based on Hyper libraries and inspired by Gunicorn." -category = "main" +version = "0.17.3" +description = "A ASGI Server based on Hyper libraries and inspired by Gunicorn" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "hypercorn-0.17.3-py3-none-any.whl", hash = "sha256:059215dec34537f9d40a69258d323f56344805efb462959e727152b0aa504547"}, + {file = "hypercorn-0.17.3.tar.gz", hash = "sha256:1b37802ee3ac52d2d85270700d565787ab16cf19e1462ccfa9f089ca17574165"}, +] [package.dependencies] +exceptiongroup = {version = ">=1.1.0", markers = "python_version < \"3.11\""} h11 = "*" h2 = ">=3.1.0" priority = "*" -toml = "*" +taskgroup = {version = "*", markers = "python_version < \"3.11\""} +tomli = {version = "*", markers = "python_version < \"3.11\""} +typing_extensions = {version = "*", markers = "python_version < \"3.11\""} wsproto = ">=0.14.0" [package.extras] +docs = ["pydata_sphinx_theme", "sphinxcontrib_mermaid"] h3 = ["aioquic (>=0.9.0,<1.0)"] -tests = ["hypothesis", "mock", "pytest", "pytest-asyncio", "pytest-cov", "pytest-trio", "trio"] -trio = ["trio (>=0.11.0)"] -uvloop = ["uvloop"] +trio = ["trio (>=0.22.0)"] +uvloop = ["uvloop (>=0.18)"] [[package]] name = "hyperframe" version = "6.0.1" description = "HTTP/2 framing layer for Python" -category = "main" optional = false python-versions = ">=3.6.1" +files = [ + {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, + {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, +] [[package]] name = "idna" -version = "3.3" +version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] [[package]] name = "importlib-metadata" -version = "4.10.1" +version = "8.0.0" description = "Read metadata from Python packages" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-8.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"}, + {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"}, +] [package.dependencies] zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] +test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] [[package]] name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" -category = "main" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" optional = false -python-versions = "*" - -[[package]] -name = "isort" -version = "5.10.1" -description = "A Python utility / library to sort Python imports." -category = "dev" -optional = false -python-versions = ">=3.6.1,<4.0" - -[package.extras] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] -colors = ["colorama (>=0.4.3,<0.5.0)"] -plugins = ["setuptools"] +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] [[package]] name = "itsdangerous" -version = "2.0.1" +version = "2.2.0" description = "Safely pass data to untrusted environments and back." -category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] [[package]] name = "jinja2" -version = "3.0.3" +version = "3.1.4" description = "A very fast and expressive template engine." -category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, + {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, +] [package.dependencies] MarkupSafe = ">=2.0" @@ -507,332 +981,926 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "lxml" -version = "4.7.1" +version = "5.3.0" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" +python-versions = ">=3.6" +files = [ + {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656"}, + {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8"}, + {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03"}, + {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7"}, + {file = "lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80"}, + {file = "lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3"}, + {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b"}, + {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080"}, + {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec"}, + {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be"}, + {file = "lxml-5.3.0-cp311-cp311-win32.whl", hash = "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9"}, + {file = "lxml-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1"}, + {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859"}, + {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c"}, + {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8"}, + {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d"}, + {file = "lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30"}, + {file = "lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f"}, + {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a"}, + {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367"}, + {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb"}, + {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b"}, + {file = "lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957"}, + {file = "lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d"}, + {file = "lxml-5.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99"}, + {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237"}, + {file = "lxml-5.3.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577"}, + {file = "lxml-5.3.0-cp36-cp36m-win32.whl", hash = "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70"}, + {file = "lxml-5.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c"}, + {file = "lxml-5.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512"}, + {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b"}, + {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5"}, + {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11"}, + {file = "lxml-5.3.0-cp37-cp37m-win32.whl", hash = "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84"}, + {file = "lxml-5.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e"}, + {file = "lxml-5.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15"}, + {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920"}, + {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945"}, + {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42"}, + {file = "lxml-5.3.0-cp38-cp38-win32.whl", hash = "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e"}, + {file = "lxml-5.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903"}, + {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de"}, + {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe"}, + {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727"}, + {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a"}, + {file = "lxml-5.3.0-cp39-cp39-win32.whl", hash = "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff"}, + {file = "lxml-5.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83"}, + {file = "lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f"}, + {file = "lxml-5.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8"}, + {file = "lxml-5.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9"}, + {file = "lxml-5.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c"}, + {file = "lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f"}, +] [package.extras] cssselect = ["cssselect (>=0.7)"] +html-clean = ["lxml-html-clean"] html5 = ["html5lib"] -htmlsoup = ["beautifulsoup4"] -source = ["Cython (>=0.29.7)"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (>=3.0.11)"] [[package]] name = "mako" -version = "1.1.6" -description = "A super-fast templating language that borrows the best ideas from the existing templating languages." -category = "main" +version = "1.3.5" +description = "A super-fast templating language that borrows the best ideas from the existing templating languages." optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.8" +files = [ + {file = "Mako-1.3.5-py3-none-any.whl", hash = "sha256:260f1dbc3a519453a9c856dedfe4beb4e50bd5a26d96386cb6c80856556bb91a"}, + {file = "Mako-1.3.5.tar.gz", hash = "sha256:48dbc20568c1d276a2698b36d968fa76161bf127194907ea6fc594fa81f943bc"}, +] [package.dependencies] MarkupSafe = ">=0.9.2" [package.extras] -babel = ["babel"] +babel = ["Babel"] lingua = ["lingua"] +testing = ["pytest"] [[package]] name = "markdown" -version = "3.3.6" -description = "Python implementation of Markdown." -category = "main" +version = "3.7" +description = "Python implementation of John Gruber's Markdown." optional = false -python-versions = ">=3.6" - -[package.dependencies] -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} +python-versions = ">=3.8" +files = [ + {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, + {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, +] [package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] testing = ["coverage", "pyyaml"] [[package]] name = "markupsafe" -version = "2.0.1" +version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" optional = false -python-versions = ">=3.6" - -[[package]] -name = "mccabe" -version = "0.6.1" -description = "McCabe checker, plugin for flake8" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "mysql-connector" -version = "2.2.9" -description = "MySQL driver written in Python" -category = "main" -optional = false -python-versions = "*" +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] [[package]] name = "mysqlclient" -version = "2.1.0" +version = "2.2.4" description = "Python interface to MySQL" -category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" +files = [ + {file = "mysqlclient-2.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:ac44777eab0a66c14cb0d38965572f762e193ec2e5c0723bcd11319cc5b693c5"}, + {file = "mysqlclient-2.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:329e4eec086a2336fe3541f1ce095d87a6f169d1cc8ba7b04ac68bcb234c9711"}, + {file = "mysqlclient-2.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:e1ebe3f41d152d7cb7c265349fdb7f1eca86ccb0ca24a90036cde48e00ceb2ab"}, + {file = "mysqlclient-2.2.4-cp38-cp38-win_amd64.whl", hash = "sha256:3c318755e06df599338dad7625f884b8a71fcf322a9939ef78c9b3db93e1de7a"}, + {file = "mysqlclient-2.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:9d4c015480c4a6b2b1602eccd9846103fc70606244788d04aa14b31c4bd1f0e2"}, + {file = "mysqlclient-2.2.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d43987bb9626096a302ca6ddcdd81feaeca65ced1d5fe892a6a66b808326aa54"}, + {file = "mysqlclient-2.2.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4e80dcad884dd6e14949ac6daf769123223a52a6805345608bf49cdaf7bc8b3a"}, + {file = "mysqlclient-2.2.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9d3310295cb682232cadc28abd172f406c718b9ada41d2371259098ae37779d3"}, + {file = "mysqlclient-2.2.4.tar.gz", hash = "sha256:33bc9fb3464e7d7c10b1eaf7336c5ff8f2a3d3b88bab432116ad2490beb3bf41"}, +] + +[[package]] +name = "opentelemetry-api" +version = "1.26.0" +description = "OpenTelemetry Python API" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_api-1.26.0-py3-none-any.whl", hash = "sha256:7d7ea33adf2ceda2dd680b18b1677e4152000b37ca76e679da71ff103b943064"}, + {file = "opentelemetry_api-1.26.0.tar.gz", hash = "sha256:2bd639e4bed5b18486fef0b5a520aaffde5a18fc225e808a1ac4df363f43a1ce"}, +] + +[package.dependencies] +deprecated = ">=1.2.6" +importlib-metadata = ">=6.0,<=8.0.0" + +[[package]] +name = "opentelemetry-exporter-otlp-proto-common" +version = "1.26.0" +description = "OpenTelemetry Protobuf encoding" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_exporter_otlp_proto_common-1.26.0-py3-none-any.whl", hash = "sha256:ee4d8f8891a1b9c372abf8d109409e5b81947cf66423fd998e56880057afbc71"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.26.0.tar.gz", hash = "sha256:bdbe50e2e22a1c71acaa0c8ba6efaadd58882e5a5978737a44a4c4b10d304c92"}, +] + +[package.dependencies] +opentelemetry-proto = "1.26.0" + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.26.0" +description = "OpenTelemetry Collector Protobuf over HTTP Exporter" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_exporter_otlp_proto_http-1.26.0-py3-none-any.whl", hash = "sha256:ee72a87c48ec977421b02f16c52ea8d884122470e0be573905237b540f4ee562"}, + {file = "opentelemetry_exporter_otlp_proto_http-1.26.0.tar.gz", hash = "sha256:5801ebbcf7b527377883e6cbbdda35ee712dc55114fff1e93dfee210be56c908"}, +] + +[package.dependencies] +deprecated = ">=1.2.6" +googleapis-common-protos = ">=1.52,<2.0" +opentelemetry-api = ">=1.15,<2.0" +opentelemetry-exporter-otlp-proto-common = "1.26.0" +opentelemetry-proto = "1.26.0" +opentelemetry-sdk = ">=1.26.0,<1.27.0" +requests = ">=2.7,<3.0" + +[[package]] +name = "opentelemetry-instrumentation" +version = "0.47b0" +description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_instrumentation-0.47b0-py3-none-any.whl", hash = "sha256:88974ee52b1db08fc298334b51c19d47e53099c33740e48c4f084bd1afd052d5"}, + {file = "opentelemetry_instrumentation-0.47b0.tar.gz", hash = "sha256:96f9885e450c35e3f16a4f33145f2ebf620aea910c9fd74a392bbc0f807a350f"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.4,<2.0" +setuptools = ">=16.0" +wrapt = ">=1.0.0,<2.0.0" + +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.47b0" +description = "ASGI instrumentation for OpenTelemetry" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_instrumentation_asgi-0.47b0-py3-none-any.whl", hash = "sha256:b798dc4957b3edc9dfecb47a4c05809036a4b762234c5071212fda39ead80ade"}, + {file = "opentelemetry_instrumentation_asgi-0.47b0.tar.gz", hash = "sha256:e78b7822c1bca0511e5e9610ec484b8994a81670375e570c76f06f69af7c506a"}, +] + +[package.dependencies] +asgiref = ">=3.0,<4.0" +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.47b0" +opentelemetry-semantic-conventions = "0.47b0" +opentelemetry-util-http = "0.47b0" + +[package.extras] +instruments = ["asgiref (>=3.0,<4.0)"] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.47b0" +description = "OpenTelemetry FastAPI Instrumentation" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_instrumentation_fastapi-0.47b0-py3-none-any.whl", hash = "sha256:5ac28dd401160b02e4f544a85a9e4f61a8cbe5b077ea0379d411615376a2bd21"}, + {file = "opentelemetry_instrumentation_fastapi-0.47b0.tar.gz", hash = "sha256:0c7c10b5d971e99a420678ffd16c5b1ea4f0db3b31b62faf305fbb03b4ebee36"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.47b0" +opentelemetry-instrumentation-asgi = "0.47b0" +opentelemetry-semantic-conventions = "0.47b0" +opentelemetry-util-http = "0.47b0" + +[package.extras] +instruments = ["fastapi (>=0.58,<1.0)", "fastapi-slim (>=0.111.0,<0.112.0)"] + +[[package]] +name = "opentelemetry-instrumentation-redis" +version = "0.47b0" +description = "OpenTelemetry Redis instrumentation" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_instrumentation_redis-0.47b0-py3-none-any.whl", hash = "sha256:169de5154cc37ccf402dd43ac06a47f7f883abba7c0a8f99b2731164ec4f1132"}, + {file = "opentelemetry_instrumentation_redis-0.47b0.tar.gz", hash = "sha256:15886c725c7e3b6f706964bd72dbfcbefeeeaa1254e98f662516cfed453aaebe"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.47b0" +opentelemetry-semantic-conventions = "0.47b0" +wrapt = ">=1.12.1" + +[package.extras] +instruments = ["redis (>=2.6)"] + +[[package]] +name = "opentelemetry-instrumentation-sqlalchemy" +version = "0.47b0" +description = "OpenTelemetry SQLAlchemy instrumentation" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_instrumentation_sqlalchemy-0.47b0-py3-none-any.whl", hash = "sha256:997b2c4a624ebcba45b9bda27882622d0ab3028d66a5fb50cdcf3581af04b3d1"}, + {file = "opentelemetry_instrumentation_sqlalchemy-0.47b0.tar.gz", hash = "sha256:bbeab06fc421ddae16bb69ca287abb81a131d3dff97de60b02c092887794103d"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.47b0" +opentelemetry-semantic-conventions = "0.47b0" +packaging = ">=21.0" +wrapt = ">=1.11.2" + +[package.extras] +instruments = ["sqlalchemy"] + +[[package]] +name = "opentelemetry-proto" +version = "1.26.0" +description = "OpenTelemetry Python Proto" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_proto-1.26.0-py3-none-any.whl", hash = "sha256:6c4d7b4d4d9c88543bcf8c28ae3f8f0448a753dc291c18c5390444c90b76a725"}, + {file = "opentelemetry_proto-1.26.0.tar.gz", hash = "sha256:c5c18796c0cab3751fc3b98dee53855835e90c0422924b484432ac852d93dc1e"}, +] + +[package.dependencies] +protobuf = ">=3.19,<5.0" + +[[package]] +name = "opentelemetry-sdk" +version = "1.26.0" +description = "OpenTelemetry Python SDK" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_sdk-1.26.0-py3-none-any.whl", hash = "sha256:feb5056a84a88670c041ea0ded9921fca559efec03905dddeb3885525e0af897"}, + {file = "opentelemetry_sdk-1.26.0.tar.gz", hash = "sha256:c90d2868f8805619535c05562d699e2f4fb1f00dbd55a86dcefca4da6fa02f85"}, +] + +[package.dependencies] +opentelemetry-api = "1.26.0" +opentelemetry-semantic-conventions = "0.47b0" +typing-extensions = ">=3.7.4" + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.47b0" +description = "OpenTelemetry Semantic Conventions" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_semantic_conventions-0.47b0-py3-none-any.whl", hash = "sha256:4ff9d595b85a59c1c1413f02bba320ce7ea6bf9e2ead2b0913c4395c7bbc1063"}, + {file = "opentelemetry_semantic_conventions-0.47b0.tar.gz", hash = "sha256:a8d57999bbe3495ffd4d510de26a97dadc1dace53e0275001b2c1b2f67992a7e"}, +] + +[package.dependencies] +deprecated = ">=1.2.6" +opentelemetry-api = "1.26.0" + +[[package]] +name = "opentelemetry-util-http" +version = "0.47b0" +description = "Web util for OpenTelemetry" +optional = false +python-versions = ">=3.8" +files = [ + {file = "opentelemetry_util_http-0.47b0-py3-none-any.whl", hash = "sha256:3d3215e09c4a723b12da6d0233a31395aeb2bb33a64d7b15a1500690ba250f19"}, + {file = "opentelemetry_util_http-0.47b0.tar.gz", hash = "sha256:352a07664c18eef827eb8ddcbd64c64a7284a39dd1655e2f16f577eb046ccb32"}, +] [[package]] name = "orjson" -version = "3.6.6" +version = "3.10.7" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "orjson-3.10.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:74f4544f5a6405b90da8ea724d15ac9c36da4d72a738c64685003337401f5c12"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34a566f22c28222b08875b18b0dfbf8a947e69df21a9ed5c51a6bf91cfb944ac"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf6ba8ebc8ef5792e2337fb0419f8009729335bb400ece005606336b7fd7bab7"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac7cf6222b29fbda9e3a472b41e6a5538b48f2c8f99261eecd60aafbdb60690c"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de817e2f5fc75a9e7dd350c4b0f54617b280e26d1631811a43e7e968fa71e3e9"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:348bdd16b32556cf8d7257b17cf2bdb7ab7976af4af41ebe79f9796c218f7e91"}, + {file = "orjson-3.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:479fd0844ddc3ca77e0fd99644c7fe2de8e8be1efcd57705b5c92e5186e8a250"}, + {file = "orjson-3.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fdf5197a21dd660cf19dfd2a3ce79574588f8f5e2dbf21bda9ee2d2b46924d84"}, + {file = "orjson-3.10.7-cp310-none-win32.whl", hash = "sha256:d374d36726746c81a49f3ff8daa2898dccab6596864ebe43d50733275c629175"}, + {file = "orjson-3.10.7-cp310-none-win_amd64.whl", hash = "sha256:cb61938aec8b0ffb6eef484d480188a1777e67b05d58e41b435c74b9d84e0b9c"}, + {file = "orjson-3.10.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7db8539039698ddfb9a524b4dd19508256107568cdad24f3682d5773e60504a2"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:480f455222cb7a1dea35c57a67578848537d2602b46c464472c995297117fa09"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a9c9b168b3a19e37fe2778c0003359f07822c90fdff8f98d9d2a91b3144d8e0"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8de062de550f63185e4c1c54151bdddfc5625e37daf0aa1e75d2a1293e3b7d9a"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b0dd04483499d1de9c8f6203f8975caf17a6000b9c0c54630cef02e44ee624e"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b58d3795dafa334fc8fd46f7c5dc013e6ad06fd5b9a4cc98cb1456e7d3558bd6"}, + {file = "orjson-3.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:33cfb96c24034a878d83d1a9415799a73dc77480e6c40417e5dda0710d559ee6"}, + {file = "orjson-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e724cebe1fadc2b23c6f7415bad5ee6239e00a69f30ee423f319c6af70e2a5c0"}, + {file = "orjson-3.10.7-cp311-none-win32.whl", hash = "sha256:82763b46053727a7168d29c772ed5c870fdae2f61aa8a25994c7984a19b1021f"}, + {file = "orjson-3.10.7-cp311-none-win_amd64.whl", hash = "sha256:eb8d384a24778abf29afb8e41d68fdd9a156cf6e5390c04cc07bbc24b89e98b5"}, + {file = "orjson-3.10.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44a96f2d4c3af51bfac6bc4ef7b182aa33f2f054fd7f34cc0ee9a320d051d41f"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76ac14cd57df0572453543f8f2575e2d01ae9e790c21f57627803f5e79b0d3c3"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bdbb61dcc365dd9be94e8f7df91975edc9364d6a78c8f7adb69c1cdff318ec93"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b48b3db6bb6e0a08fa8c83b47bc169623f801e5cc4f24442ab2b6617da3b5313"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23820a1563a1d386414fef15c249040042b8e5d07b40ab3fe3efbfbbcbcb8864"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0c6a008e91d10a2564edbb6ee5069a9e66df3fbe11c9a005cb411f441fd2c09"}, + {file = "orjson-3.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d352ee8ac1926d6193f602cbe36b1643bbd1bbcb25e3c1a657a4390f3000c9a5"}, + {file = "orjson-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d2d9f990623f15c0ae7ac608103c33dfe1486d2ed974ac3f40b693bad1a22a7b"}, + {file = "orjson-3.10.7-cp312-none-win32.whl", hash = "sha256:7c4c17f8157bd520cdb7195f75ddbd31671997cbe10aee559c2d613592e7d7eb"}, + {file = "orjson-3.10.7-cp312-none-win_amd64.whl", hash = "sha256:1d9c0e733e02ada3ed6098a10a8ee0052dd55774de3d9110d29868d24b17faa1"}, + {file = "orjson-3.10.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:77d325ed866876c0fa6492598ec01fe30e803272a6e8b10e992288b009cbe149"}, + {file = "orjson-3.10.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ea2c232deedcb605e853ae1db2cc94f7390ac776743b699b50b071b02bea6fe"}, + {file = "orjson-3.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3dcfbede6737fdbef3ce9c37af3fb6142e8e1ebc10336daa05872bfb1d87839c"}, + {file = "orjson-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11748c135f281203f4ee695b7f80bb1358a82a63905f9f0b794769483ea854ad"}, + {file = "orjson-3.10.7-cp313-none-win32.whl", hash = "sha256:a7e19150d215c7a13f39eb787d84db274298d3f83d85463e61d277bbd7f401d2"}, + {file = "orjson-3.10.7-cp313-none-win_amd64.whl", hash = "sha256:eef44224729e9525d5261cc8d28d6b11cafc90e6bd0be2157bde69a52ec83024"}, + {file = "orjson-3.10.7-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6ea2b2258eff652c82652d5e0f02bd5e0463a6a52abb78e49ac288827aaa1469"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:430ee4d85841e1483d487e7b81401785a5dfd69db5de01314538f31f8fbf7ee1"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4b6146e439af4c2472c56f8540d799a67a81226e11992008cb47e1267a9b3225"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:084e537806b458911137f76097e53ce7bf5806dda33ddf6aaa66a028f8d43a23"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4829cf2195838e3f93b70fd3b4292156fc5e097aac3739859ac0dcc722b27ac0"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1193b2416cbad1a769f868b1749535d5da47626ac29445803dae7cc64b3f5c98"}, + {file = "orjson-3.10.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4e6c3da13e5a57e4b3dca2de059f243ebec705857522f188f0180ae88badd354"}, + {file = "orjson-3.10.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c31008598424dfbe52ce8c5b47e0752dca918a4fdc4a2a32004efd9fab41d866"}, + {file = "orjson-3.10.7-cp38-none-win32.whl", hash = "sha256:7122a99831f9e7fe977dc45784d3b2edc821c172d545e6420c375e5a935f5a1c"}, + {file = "orjson-3.10.7-cp38-none-win_amd64.whl", hash = "sha256:a763bc0e58504cc803739e7df040685816145a6f3c8a589787084b54ebc9f16e"}, + {file = "orjson-3.10.7-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e76be12658a6fa376fcd331b1ea4e58f5a06fd0220653450f0d415b8fd0fbe20"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed350d6978d28b92939bfeb1a0570c523f6170efc3f0a0ef1f1df287cd4f4960"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:144888c76f8520e39bfa121b31fd637e18d4cc2f115727865fdf9fa325b10412"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09b2d92fd95ad2402188cf51573acde57eb269eddabaa60f69ea0d733e789fe9"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b24a579123fa884f3a3caadaed7b75eb5715ee2b17ab5c66ac97d29b18fe57f"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591bcfe7512353bd609875ab38050efe3d55e18934e2f18950c108334b4ff"}, + {file = "orjson-3.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f4db56635b58cd1a200b0a23744ff44206ee6aa428185e2b6c4a65b3197abdcd"}, + {file = "orjson-3.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0fa5886854673222618638c6df7718ea7fe2f3f2384c452c9ccedc70b4a510a5"}, + {file = "orjson-3.10.7-cp39-none-win32.whl", hash = "sha256:8272527d08450ab16eb405f47e0f4ef0e5ff5981c3d82afe0efd25dcbef2bcd2"}, + {file = "orjson-3.10.7-cp39-none-win_amd64.whl", hash = "sha256:974683d4618c0c7dbf4f69c95a979734bf183d0658611760017f6e70a145af58"}, + {file = "orjson-3.10.7.tar.gz", hash = "sha256:75ef0640403f945f3a1f9f6400686560dbfb0fb5b16589ad62cd477043c4eee3"}, +] [[package]] name = "packaging" -version = "21.3" +version = "24.1" description = "Core utilities for Python packages" -category = "main" optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] [[package]] name = "paginate" version = "0.5.6" description = "Divides large result sets into pages for easier browsing" -category = "main" optional = false python-versions = "*" +files = [ + {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, +] [[package]] name = "parse" -version = "1.19.0" +version = "1.20.2" description = "parse() is the opposite of format()" -category = "main" optional = false python-versions = "*" +files = [ + {file = "parse-1.20.2-py2.py3-none-any.whl", hash = "sha256:967095588cb802add9177d0c0b6133b5ba33b1ea9007ca800e526f42a85af558"}, + {file = "parse-1.20.2.tar.gz", hash = "sha256:b41d604d16503c79d81af5165155c0b20f6c8d6c559efa66b4b695c3e5a0a0ce"}, +] [[package]] name = "pluggy" -version = "1.0.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" -category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "poetry-dynamic-versioning" -version = "0.13.1" -description = "Plugin for Poetry to enable dynamic versioning based on VCS tags" -category = "main" -optional = false -python-versions = ">=3.5,<4.0" - -[package.dependencies] -dunamai = ">=1.5,<2.0" -jinja2 = {version = ">=2.11.1,<4", markers = "python_version >= \"3.6\" and python_version < \"4.0\""} -tomlkit = ">=0.4" - [[package]] name = "posix-ipc" -version = "1.0.5" +version = "1.1.1" description = "POSIX IPC primitives (semaphores, shared memory and message queues) for Python" -category = "main" optional = false python-versions = "*" +files = [ + {file = "posix_ipc-1.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0c2019e462c5e556568ec7947a0dd66abcd516a6f4939aed545daedc5c7b0b78"}, + {file = "posix_ipc-1.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:516259a7f1b1ba49a16ebe06ae23e9246162cb26e3eb825e6535c487e67478d2"}, + {file = "posix_ipc-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:edfd47a68b0022c14e511ca4d600ff1f2e44457695002a56accfa3535be046b1"}, + {file = "posix_ipc-1.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:3c21f6f459b399badc12dec9de73274d42aa8708e8a56146d79faca27e362480"}, + {file = "posix_ipc-1.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5b9a38467f11c040a2e15e5cc7236494dc4ba7dfb4151bffef493a1f88fbb2c1"}, + {file = "posix_ipc-1.1.1.tar.gz", hash = "sha256:e2456ba0cfb2ee5ba14121450e8d825b3c4a1461fca0761220aab66d4111cbb7"}, +] [[package]] name = "priority" version = "2.0.0" description = "A pure-Python implementation of the HTTP/2 priority tree" -category = "main" optional = false python-versions = ">=3.6.1" +files = [ + {file = "priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa"}, + {file = "priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0"}, +] [[package]] name = "prometheus-client" -version = "0.13.1" +version = "0.20.0" description = "Python client for the Prometheus monitoring system." -category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +files = [ + {file = "prometheus_client-0.20.0-py3-none-any.whl", hash = "sha256:cde524a85bce83ca359cc837f28b8c0db5cac7aa653a588fd7e84ba061c329e7"}, + {file = "prometheus_client-0.20.0.tar.gz", hash = "sha256:287629d00b147a32dcb2be0b9df905da599b2d82f80377083ec8463309a4bb89"}, +] [package.extras] twisted = ["twisted"] [[package]] name = "prometheus-fastapi-instrumentator" -version = "5.7.1" -description = "Instrument your FastAPI with Prometheus metrics" -category = "main" +version = "7.0.0" +description = "Instrument your FastAPI with Prometheus metrics." optional = false -python-versions = ">=3.6.0,<4.0.0" +python-versions = ">=3.8.1,<4.0.0" +files = [ + {file = "prometheus_fastapi_instrumentator-7.0.0-py3-none-any.whl", hash = "sha256:96030c43c776ee938a3dae58485ec24caed7e05bfc60fe067161e0d5b5757052"}, + {file = "prometheus_fastapi_instrumentator-7.0.0.tar.gz", hash = "sha256:5ba67c9212719f244ad7942d75ded80693b26331ee5dfc1e7571e4794a9ccbed"}, +] [package.dependencies] -fastapi = ">=0.38.1,<1.0.0" prometheus-client = ">=0.8.0,<1.0.0" +starlette = ">=0.30.0,<1.0.0" [[package]] name = "protobuf" -version = "3.19.4" -description = "Protocol Buffers" -category = "main" +version = "4.25.4" +description = "" optional = false -python-versions = ">=3.5" - -[[package]] -name = "py" -version = "1.11.0" -description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.8" +files = [ + {file = "protobuf-4.25.4-cp310-abi3-win32.whl", hash = "sha256:db9fd45183e1a67722cafa5c1da3e85c6492a5383f127c86c4c4aa4845867dc4"}, + {file = "protobuf-4.25.4-cp310-abi3-win_amd64.whl", hash = "sha256:ba3d8504116a921af46499471c63a85260c1a5fc23333154a427a310e015d26d"}, + {file = "protobuf-4.25.4-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:eecd41bfc0e4b1bd3fa7909ed93dd14dd5567b98c941d6c1ad08fdcab3d6884b"}, + {file = "protobuf-4.25.4-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:4c8a70fdcb995dcf6c8966cfa3a29101916f7225e9afe3ced4395359955d3835"}, + {file = "protobuf-4.25.4-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:3319e073562e2515c6ddc643eb92ce20809f5d8f10fead3332f71c63be6a7040"}, + {file = "protobuf-4.25.4-cp38-cp38-win32.whl", hash = "sha256:7e372cbbda66a63ebca18f8ffaa6948455dfecc4e9c1029312f6c2edcd86c4e1"}, + {file = "protobuf-4.25.4-cp38-cp38-win_amd64.whl", hash = "sha256:051e97ce9fa6067a4546e75cb14f90cf0232dcb3e3d508c448b8d0e4265b61c1"}, + {file = "protobuf-4.25.4-cp39-cp39-win32.whl", hash = "sha256:90bf6fd378494eb698805bbbe7afe6c5d12c8e17fca817a646cd6a1818c696ca"}, + {file = "protobuf-4.25.4-cp39-cp39-win_amd64.whl", hash = "sha256:ac79a48d6b99dfed2729ccccee547b34a1d3d63289c71cef056653a846a2240f"}, + {file = "protobuf-4.25.4-py3-none-any.whl", hash = "sha256:bfbebc1c8e4793cfd58589acfb8a1026be0003e852b9da7db5a4285bde996978"}, + {file = "protobuf-4.25.4.tar.gz", hash = "sha256:0dc4a62cc4052a036ee2204d26fe4d835c62827c855c8a03f29fe6da146b380d"}, +] [[package]] name = "pyalpm" version = "0.10.6" description = "libalpm bindings for Python 3" -category = "main" optional = false python-versions = "*" - -[[package]] -name = "pycodestyle" -version = "2.8.0" -description = "Python style guide checker" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "pyalpm-0.10.6.tar.gz", hash = "sha256:99e6ec73b8c46bb12466013f228f831ee0d18e8ab664b91a01c2a3c40de07c7f"}, +] [[package]] name = "pycparser" -version = "2.21" +version = "2.22" description = "C parser in Python" -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] [[package]] name = "pydantic" -version = "1.9.0" -description = "Data validation and settings management using python 3.6 type hinting" -category = "main" +version = "2.8.2" +description = "Data validation using Python type hints" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, + {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, +] [package.dependencies] -typing-extensions = ">=3.7.4.3" +annotated-types = ">=0.4.0" +pydantic-core = "2.20.1" +typing-extensions = [ + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, + {version = ">=4.6.1", markers = "python_version < \"3.13\""}, +] [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] [[package]] -name = "pyflakes" -version = "2.4.0" -description = "passive checker of Python programs" -category = "dev" +name = "pydantic-core" +version = "2.20.1" +description = "Core functionality for Pydantic validation and serialization" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, + {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, + {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, + {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, + {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, + {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, + {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, + {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, + {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, + {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, + {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, + {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, + {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, + {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pygit2" -version = "1.7.2" +version = "1.17.0" description = "Python bindings for libgit2." -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" +files = [ + {file = "pygit2-1.17.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cbe1a3354a3eff0f4e842abcff73b24455ba7205ac959f146d7cb8dcd63cfa45"}, + {file = "pygit2-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:578d78fc97d5c16b1ad44c1e2fda093628c3f29793b42be68b93a46ce7a662a0"}, + {file = "pygit2-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e59de6138787aa3a5365557fb1ad427d3e877868917e0910257fb11f71f3c736"}, + {file = "pygit2-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66431dba77977b9010fac580eaefcc7567ea0955b030d8e8472fdce0f1a7c5f0"}, + {file = "pygit2-1.17.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:877ce82239de45301fa9953d50b41c46a5300cf7b3993db86bde64e08521a506"}, + {file = "pygit2-1.17.0-cp310-cp310-win32.whl", hash = "sha256:8b7921016cbec166083206daca20d00f2358b18178122e1b0d1932b410296de1"}, + {file = "pygit2-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:5724683b3d239cc1457b9800d9d7988a00cd0cb1797d5caa6f46d16f3f74f1ca"}, + {file = "pygit2-1.17.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:39e7087e2affdba2530d1fe1ec04c27fa85db405be84e1cd4759891045324a31"}, + {file = "pygit2-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ef5c3634317295268ef84b99e8acae37cb2f8b17d966b318c79e5211bf78d3"}, + {file = "pygit2-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ad964b2eea81e0c99d05c6ec0ec8b5715d3d0c99b3b6e09abcdb57c57701592"}, + {file = "pygit2-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cce8db6aa40361270b14e1adb3a8e7d4606b9b53088e27321472c9a92f922648"}, + {file = "pygit2-1.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:136b1ea44107fb6a3a58e7b1322227e83101bff50a929816d8653a38e0ed0e07"}, + {file = "pygit2-1.17.0-cp311-cp311-win32.whl", hash = "sha256:5ffce0772167e5c436be57ee3ef0fb421c864657911aeb1a97618e1dd9f8b574"}, + {file = "pygit2-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:0809029cf804f343abdc9eaeaf9d915f9dbf320d79078c20138a3bf642583365"}, + {file = "pygit2-1.17.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f7224d89a7dda7290e458393941e500c8682f375f41e6d80ee423958a5d4013d"}, + {file = "pygit2-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9ae1967b0c8a2438b3b0e4a63307b5c22c80024a2f09b28d14dfde0001fed8dc"}, + {file = "pygit2-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:507343fa142a82028c8448c2626317dc19885985aba8ea27d381777ac484eefb"}, + {file = "pygit2-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc04917a680591c6e801df912d7fb722c253b5ac68178ff37b5666dafd06999"}, + {file = "pygit2-1.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7bb1b623cbd16962c3a1ec7f8e1012fa224c9e9642758c65e8e656ecc7ff1574"}, + {file = "pygit2-1.17.0-cp312-cp312-win32.whl", hash = "sha256:3029331ddf56a6908547278ab4c354b2d6932eb6a53be81e0093adc98a0ae540"}, + {file = "pygit2-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:1011236bab7317b82e6cbc3dff4be8467923b1dcf2ffe28bf2e64805dcb37749"}, + {file = "pygit2-1.17.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ce938e7a4fdfc816ffceb62babad65fb62e1a5ad261e880b9a072e8da144ccca"}, + {file = "pygit2-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61ff2c8b0fc96fdf45a7a5239cc262b0293a5171f68d67eea239a42c3b2226cb"}, + {file = "pygit2-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8101aa723c292892ba46303b19487a9fb0de50d9e30f4c1c2a76e3383b6e4b6d"}, + {file = "pygit2-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36e3e9225e3f01bb6a2d4589c126900bbc571cd0876ca9c01372a6e3d3693c0e"}, + {file = "pygit2-1.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:614cfddbf048900da19b016787f153d44ea9fd7ef80f9e03a77024aa1555d5f4"}, + {file = "pygit2-1.17.0-cp313-cp313-win32.whl", hash = "sha256:1391762153af9715ed1d0586e3f207c518f03f5874e1f5b8e398697d006a0a82"}, + {file = "pygit2-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:d677d6fb85c426c5f5f8409bdc5a2e391016c99f73b97779b284c4ad25aa75fa"}, + {file = "pygit2-1.17.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c491db4f71fd5d814023f2ad89ad7023c15738bcbe0807acc2313026600bf1b1"}, + {file = "pygit2-1.17.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89ff254387d23d107dd2b542907d248a3a988e3be8cda99bcc8af04f56e6e5cf"}, + {file = "pygit2-1.17.0.tar.gz", hash = "sha256:fa2bc050b2c2d3e73b54d6d541c792178561a344f07e409f532d5bb97ac7b894"}, +] [package.dependencies] -cffi = ">=1.4.0" - -[[package]] -name = "pyparsing" -version = "3.0.7" -description = "Python parsing module" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] +cffi = ">=1.17.0" [[package]] name = "pytest" -version = "6.2.5" +version = "8.3.2" description = "pytest: simple powerful testing with Python" -category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, + {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, +] [package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} -attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -toml = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-asyncio" -version = "0.16.0" -description = "Pytest support for asyncio." -category = "dev" +version = "0.23.8" +description = "Pytest support for asyncio" optional = false -python-versions = ">= 3.6" +python-versions = ">=3.8" +files = [ + {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, + {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, +] [package.dependencies] -pytest = ">=5.4.0" +pytest = ">=7.0.0,<9" [package.extras] -testing = ["coverage", "hypothesis (>=5.7.1)"] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" -version = "3.0.0" +version = "5.0.0" description = "Pytest plugin for measuring coverage." -category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +files = [ + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, +] [package.dependencies] coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] - -[[package]] -name = "pytest-forked" -version = "1.4.0" -description = "run tests in isolated forked subprocesses" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -py = "*" -pytest = ">=3.10" +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-tap" -version = "3.3" +version = "3.4" description = "Test Anything Protocol (TAP) reporting plugin for pytest" -category = "dev" optional = false python-versions = "*" +files = [ + {file = "pytest-tap-3.4.tar.gz", hash = "sha256:a7c2a4a3e8b4bf18522e46d74208f8579a191dd972c59182104ad9a4967318fb"}, + {file = "pytest_tap-3.4-py3-none-any.whl", hash = "sha256:d97a2115c94415086f6faec395d243b3c18ea846ce1c1653a4b2588082be35d8"}, +] [package.dependencies] pytest = ">=3.0" @@ -840,16 +1908,18 @@ pytest = ">=3.0" [[package]] name = "pytest-xdist" -version = "2.5.0" -description = "pytest xdist plugin for distributed testing and loop-on-failing modes" -category = "main" +version = "3.6.1" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +files = [ + {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, + {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, +] [package.dependencies] -execnet = ">=1.1" -pytest = ">=6.2.0" -pytest-forked = "*" +execnet = ">=2.1" +pytest = ">=7.0.0" [package.extras] psutil = ["psutil (>=3.0)"] @@ -858,1053 +1928,542 @@ testing = ["filelock"] [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] [package.dependencies] six = ">=1.5" [[package]] name = "python-multipart" -version = "0.0.5" +version = "0.0.19" description = "A streaming multipart parser for Python" -category = "main" optional = false -python-versions = "*" - -[package.dependencies] -six = ">=1.4.0" +python-versions = ">=3.8" +files = [ + {file = "python_multipart-0.0.19-py3-none-any.whl", hash = "sha256:f8d5b0b9c618575bf9df01c684ded1d94a338839bdd8223838afacfb4bb2082d"}, + {file = "python_multipart-0.0.19.tar.gz", hash = "sha256:905502ef39050557b7a6af411f454bc19526529ca46ae6831508438890ce12cc"}, +] [[package]] name = "redis" -version = "3.5.3" -description = "Python client for Redis key-value store" -category = "main" +version = "5.0.8" +description = "Python client for Redis database and key-value store" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.7" +files = [ + {file = "redis-5.0.8-py3-none-any.whl", hash = "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4"}, + {file = "redis-5.0.8.tar.gz", hash = "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} [package.extras] -hiredis = ["hiredis (>=0.1.3)"] +hiredis = ["hiredis (>1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] [[package]] name = "requests" -version = "2.27.1" +version = "2.32.3" description = "Python HTTP for Humans." -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""} -idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""} -urllib3 = ">=1.21.1,<1.27" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" [package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] -name = "rfc3986" -version = "1.5.0" -description = "Validating URI References per RFC 3986" -category = "main" +name = "setuptools" +version = "72.2.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = "*" - -[package.dependencies] -idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} +python-versions = ">=3.8" +files = [ + {file = "setuptools-72.2.0-py3-none-any.whl", hash = "sha256:f11dd94b7bae3a156a95ec151f24e4637fb4fa19c878e4d191bfb8b2d82728c4"}, + {file = "setuptools-72.2.0.tar.gz", hash = "sha256:80aacbf633704e9c8bfa1d99fa5dd4dc59573efcf9e4042c13d3bcef91ac2ef9"}, +] [package.extras] -idna2008 = ["idna"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "ordered-set (>=3.1.1)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] [[package]] name = "sniffio" -version = "1.2.0" +version = "1.3.1" description = "Sniff out which async library your code is running under" -category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] [[package]] name = "sortedcontainers" version = "2.4.0" description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" -category = "main" optional = false python-versions = "*" +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] [[package]] name = "sqlalchemy" -version = "1.4.31" +version = "1.4.53" description = "Database Abstraction Library" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "SQLAlchemy-1.4.53-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:b61ac5457d91b5629a3dea2b258deb4cdd35ac8f6fa2031d2b9b2fff5b3396da"}, + {file = "SQLAlchemy-1.4.53-cp310-cp310-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a96aa8d425047551676b0e178ddb0683421e78eda879ab55775128b2e612cae"}, + {file = "SQLAlchemy-1.4.53-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e10ac36f0b994235c13388b39598bf27219ec8bdea5be99bdac612b01cbe525"}, + {file = "SQLAlchemy-1.4.53-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:437592b341a3229dd0443c9c803b0bf0a466f8f539014fef6cdb9c06b7edb7f9"}, + {file = "SQLAlchemy-1.4.53-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:784272ceb5eb71421fea9568749bcbe8bd019261a0e2e710a7efa76057af2499"}, + {file = "SQLAlchemy-1.4.53-cp310-cp310-win32.whl", hash = "sha256:122d7b5722df1a24402c6748bbb04687ef981493bb559d0cc0beffe722e0e6ed"}, + {file = "SQLAlchemy-1.4.53-cp310-cp310-win_amd64.whl", hash = "sha256:4604d42b2abccba266d3f5bbe883684b5df93e74054024c70d3fbb5eea45e530"}, + {file = "SQLAlchemy-1.4.53-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fb8e15dfa47f5de11ab073e12aadd6b502cfb7ac4bafd18bd18cfd1c7d13dbbc"}, + {file = "SQLAlchemy-1.4.53-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc8be4df55e8fde3006d9cb1f6b3df2ba26db613855dc4df2c0fcd5ec15cb3b7"}, + {file = "SQLAlchemy-1.4.53-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86b11640251f9a9789fd96cd6e5d176b1c230230c70ad40299bcbcc568451b4c"}, + {file = "SQLAlchemy-1.4.53-cp311-cp311-win32.whl", hash = "sha256:cd534c716f86bdf95b7b984a34ee278c91d1b1d7d183e7e5ff878600b1696046"}, + {file = "SQLAlchemy-1.4.53-cp311-cp311-win_amd64.whl", hash = "sha256:6dd06572872ca13ef5a90306a3e5af787498ddaa17fb00109b1243642646cd69"}, + {file = "SQLAlchemy-1.4.53-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:2774c24c405136c3ef472e2352bdca7330659d481fbf2283f996c0ef9eb90f22"}, + {file = "SQLAlchemy-1.4.53-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68a614765197b3d13a730d631a78c3bb9b3b72ba58ed7ab295d58d517464e315"}, + {file = "SQLAlchemy-1.4.53-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d13d4dfbc6e52363886b47cf02cf68c5d2a37c468626694dc210d7e97d4ad330"}, + {file = "SQLAlchemy-1.4.53-cp312-cp312-win32.whl", hash = "sha256:197065b91456574d70b6459bfa62bc0b52a4960a29ef923c375ec427274a3e05"}, + {file = "SQLAlchemy-1.4.53-cp312-cp312-win_amd64.whl", hash = "sha256:421306c4b936b0271a3ce2dc074928d5ece4a36f9c482daa5770f44ecfc3a883"}, + {file = "SQLAlchemy-1.4.53-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:13fc34b35d8ddb3fbe3f8fcfdf6c2546e676187f0fb20f5774da362ddaf8fa2d"}, + {file = "SQLAlchemy-1.4.53-cp36-cp36m-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626be971ff89541cfd3e70b54be00b57a7f8557204decb6223ce0428fec058f3"}, + {file = "SQLAlchemy-1.4.53-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:991e42fdfec561ebc6a4fae7161a86d129d6069fa14210b96b8dd752afa7059c"}, + {file = "SQLAlchemy-1.4.53-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:95123f3a1e0e8020848fd32ba751db889a01a44e4e4fef7e58c87ddd0b2fca59"}, + {file = "SQLAlchemy-1.4.53-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c58e011e9e6373b3a091d83f20601fb335a3b4bace80bfcb914ac168aad3b70d"}, + {file = "SQLAlchemy-1.4.53-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:670c7769bf5dcae9aff331247b5d82fe635c63731088a46ce68ba2ba519ef36e"}, + {file = "SQLAlchemy-1.4.53-cp37-cp37m-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07ba54f09033d387ae9df8d62cbe211ed7304e0bfbece1f8c55e21db9fae5c11"}, + {file = "SQLAlchemy-1.4.53-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a38834b4c183c33daf58544281395aad2e985f0b47cca1e88ea5ada88344e63"}, + {file = "SQLAlchemy-1.4.53-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:616492f5315128a847f293a7c552f3561ac7e996d2aa5dc46bef4fb0d3781f1d"}, + {file = "SQLAlchemy-1.4.53-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0cf8c0af9563892c6632f7343bc393dfce6eeef8e4d10c5fadba9c0390520bd"}, + {file = "SQLAlchemy-1.4.53-cp37-cp37m-win32.whl", hash = "sha256:c05fe05941424c2f3747a8952381b7725e24cba2ca00141380e54789d5b616b6"}, + {file = "SQLAlchemy-1.4.53-cp37-cp37m-win_amd64.whl", hash = "sha256:93e90aa3e3b2f8e8cbae4d5509f8e0cf82972378d323c740a8df1c1e9f484172"}, + {file = "SQLAlchemy-1.4.53-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:9d7368df54d3ed45a18955f6cec38ebe075290594ac0d5c87a8ddaff7e10de27"}, + {file = "SQLAlchemy-1.4.53-cp38-cp38-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89d8ac4158ef68eea8bb0f6dd0583127d9aa8720606964ba8eee20b254f9c83a"}, + {file = "SQLAlchemy-1.4.53-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16bb9fa4d00b4581b14d9f0e2224dc7745b854aa4687738279af0f48f7056c98"}, + {file = "SQLAlchemy-1.4.53-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4fe5168d0249c23f537950b6d75935ff2709365a113e29938a979aec36668ecf"}, + {file = "SQLAlchemy-1.4.53-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b8608d162d3bd29d807aab32c3fb6e2f8e225a43d1c54c917fed38513785380"}, + {file = "SQLAlchemy-1.4.53-cp38-cp38-win32.whl", hash = "sha256:a9d4d132198844bd6828047135ce7b887687c92925049a2468a605fc775c7a1a"}, + {file = "SQLAlchemy-1.4.53-cp38-cp38-win_amd64.whl", hash = "sha256:c15d1f1fcf1f9bec0499ae1d9132b950fcc7730f2d26d10484c8808b4e077816"}, + {file = "SQLAlchemy-1.4.53-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:edf094a20a386ff2ec73de65ef18014b250259cb860edc61741e240ca22d6981"}, + {file = "SQLAlchemy-1.4.53-cp39-cp39-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83a9c3514ff19d9d30d8a8d378b24cd1dfa5528d20891481cb5f196117db6a48"}, + {file = "SQLAlchemy-1.4.53-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaaeedbceb4dfd688fff2faf25a9a87a391f548811494f7bff7fa701b639abc3"}, + {file = "SQLAlchemy-1.4.53-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d021699b9007deb7aa715629078830c99a5fec2753d9bdd5ff33290d363ef755"}, + {file = "SQLAlchemy-1.4.53-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0465b8a68f8f4de754c1966c45b187ac784ad97bc9747736f913130f0e1adea0"}, + {file = "SQLAlchemy-1.4.53-cp39-cp39-win32.whl", hash = "sha256:5f67b9e9dcac3241781e96575468d55a42332157dee04bdbf781df573dff5f85"}, + {file = "SQLAlchemy-1.4.53-cp39-cp39-win_amd64.whl", hash = "sha256:a8c2f2a0b2c4e3b86eb58c9b6bb98548205eea2fba9dae4edfd29dc6aebbe95a"}, + {file = "SQLAlchemy-1.4.53.tar.gz", hash = "sha256:5e6ab710c4c064755fd92d1a417bef360228a19bdf0eee32b03aa0f5f8e9fe0d"}, +] [package.dependencies] greenlet = {version = "!=0.4.17", markers = "python_version >= \"3\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} [package.extras] -aiomysql = ["greenlet (!=0.4.17)", "aiomysql"] -aiosqlite = ["typing_extensions (!=3.10.0.1)", "greenlet (!=0.4.17)", "aiosqlite"] +aiomysql = ["aiomysql (>=0.2.0)", "greenlet (!=0.4.17)"] +aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"] asyncio = ["greenlet (!=0.4.17)"] -asyncmy = ["greenlet (!=0.4.17)", "asyncmy (>=0.2.3)"] -mariadb_connector = ["mariadb (>=1.0.1)"] +asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"] +mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)", "mariadb (>=1.0.1,!=1.1.2)"] mssql = ["pyodbc"] -mssql_pymssql = ["pymssql"] -mssql_pyodbc = ["pyodbc"] -mypy = ["sqlalchemy2-stubs", "mypy (>=0.910)"] -mysql = ["mysqlclient (>=1.4.0,<2)", "mysqlclient (>=1.4.0)"] -mysql_connector = ["mysql-connector-python"] -oracle = ["cx_oracle (>=7,<8)", "cx_oracle (>=7)"] +mssql-pymssql = ["pymssql", "pymssql"] +mssql-pyodbc = ["pyodbc", "pyodbc"] +mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"] +mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"] +mysql-connector = ["mysql-connector-python", "mysql-connector-python"] +oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"] postgresql = ["psycopg2 (>=2.7)"] -postgresql_asyncpg = ["greenlet (!=0.4.17)", "asyncpg"] -postgresql_pg8000 = ["pg8000 (>=1.16.6)"] -postgresql_psycopg2binary = ["psycopg2-binary"] -postgresql_psycopg2cffi = ["psycopg2cffi"] -pymysql = ["pymysql (<1)", "pymysql"] -sqlcipher = ["sqlcipher3-binary"] +postgresql-asyncpg = ["asyncpg", "asyncpg", "greenlet (!=0.4.17)", "greenlet (!=0.4.17)"] +postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)", "pg8000 (>=1.16.6,!=1.29.0)"] +postgresql-psycopg2binary = ["psycopg2-binary"] +postgresql-psycopg2cffi = ["psycopg2cffi"] +pymysql = ["pymysql", "pymysql (<1)"] +sqlcipher = ["sqlcipher3_binary"] [[package]] name = "srcinfo" -version = "0.0.8" +version = "0.1.2" description = "A small library to parse .SRCINFO files" -category = "main" optional = false -python-versions = "*" +python-versions = ">=3.7,<4.0" +files = [ + {file = "srcinfo-0.1.2-py3-none-any.whl", hash = "sha256:f670a3473db3efa7392bd68add147650fb6c3e9b2ec54c1db252d80698c968da"}, + {file = "srcinfo-0.1.2.tar.gz", hash = "sha256:c8296c710015a6b25c1dc750a494841a8094a2cf4fdb33a375fbe9720ec976e6"}, +] [package.dependencies] -parse = "*" +parse = ">=1.19.0,<2.0.0" [[package]] name = "starlette" -version = "0.17.1" +version = "0.38.2" description = "The little ASGI library that shines." -category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +files = [ + {file = "starlette-0.38.2-py3-none-any.whl", hash = "sha256:4ec6a59df6bbafdab5f567754481657f7ed90dc9d69b0c9ff017907dd54faeff"}, + {file = "starlette-0.38.2.tar.gz", hash = "sha256:c7c0441065252160993a1a37cf2a73bb64d271b17303e0b0c1eb7191cfb12d75"}, +] [package.dependencies] -anyio = ">=3.0.0,<4" +anyio = ">=3.4.0,<5" [package.extras] -full = ["itsdangerous", "jinja2", "python-multipart", "pyyaml", "requests"] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] [[package]] -name = "tap.py" +name = "tap-py" version = "3.1" description = "Test Anything Protocol (TAP) tools" -category = "dev" optional = false python-versions = "*" +files = [ + {file = "tap.py-3.1-py3-none-any.whl", hash = "sha256:928c852f3361707b796c93730cc5402c6378660b161114461066acf53d65bf5d"}, + {file = "tap.py-3.1.tar.gz", hash = "sha256:3c0cd45212ad5a25b35445964e2517efa000a118a1bfc3437dae828892eaf1e1"}, +] [package.extras] -yaml = ["more-itertools", "PyYAML (>=5.1)"] +yaml = ["PyYAML (>=5.1)", "more-itertools"] [[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "main" +name = "taskgroup" +version = "0.0.0a4" +description = "backport of asyncio.TaskGroup, asyncio.Runner and asyncio.timeout" optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "*" +files = [ + {file = "taskgroup-0.0.0a4-py2.py3-none-any.whl", hash = "sha256:5c1bd0e4c06114e7a4128583ab75c987597d5378a33948a3b74c662b90f61277"}, + {file = "taskgroup-0.0.0a4.tar.gz", hash = "sha256:eb08902d221e27661950f2a0320ddf3f939f579279996f81fe30779bca3a159c"}, +] + +[package.dependencies] +exceptiongroup = "*" [[package]] name = "tomli" -version = "2.0.0" +version = "2.0.1" description = "A lil' TOML parser" -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] [[package]] name = "tomlkit" -version = "0.9.0" +version = "0.13.2" description = "Style preserving TOML library" -category = "main" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.8" +files = [ + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, +] [[package]] name = "typing-extensions" -version = "4.0.1" -description = "Backported and Experimental Type Hints for Python 3.6+" -category = "main" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] [[package]] name = "urllib3" -version = "1.26.8" +version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, +] [package.extras] -brotli = ["brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.15.0" +version = "0.30.6" description = "The lightning-fast ASGI server." -category = "main" optional = false -python-versions = "*" +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"}, + {file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"}, +] [package.dependencies] -asgiref = ">=3.4.0" click = ">=7.0" h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] -standard = ["websockets (>=9.1)", "httptools (>=0.2.0,<0.3.0)", "watchgod (>=0.6)", "python-dotenv (>=0.13)", "PyYAML (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "colorama (>=0.4)"] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "watchfiles" +version = "1.0.4" +description = "Simple, modern and high performance file watching and code reload in python." +optional = false +python-versions = ">=3.9" +files = [ + {file = "watchfiles-1.0.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ba5bb3073d9db37c64520681dd2650f8bd40902d991e7b4cfaeece3e32561d08"}, + {file = "watchfiles-1.0.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f25d0ba0fe2b6d2c921cf587b2bf4c451860086534f40c384329fb96e2044d1"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47eb32ef8c729dbc4f4273baece89398a4d4b5d21a1493efea77a17059f4df8a"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:076f293100db3b0b634514aa0d294b941daa85fc777f9c698adb1009e5aca0b1"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1eacd91daeb5158c598fe22d7ce66d60878b6294a86477a4715154990394c9b3"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13c2ce7b72026cfbca120d652f02c7750f33b4c9395d79c9790b27f014c8a5a2"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90192cdc15ab7254caa7765a98132a5a41471cf739513cc9bcf7d2ffcc0ec7b2"}, + {file = "watchfiles-1.0.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278aaa395f405972e9f523bd786ed59dfb61e4b827856be46a42130605fd0899"}, + {file = "watchfiles-1.0.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a462490e75e466edbb9fc4cd679b62187153b3ba804868452ef0577ec958f5ff"}, + {file = "watchfiles-1.0.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8d0d0630930f5cd5af929040e0778cf676a46775753e442a3f60511f2409f48f"}, + {file = "watchfiles-1.0.4-cp310-cp310-win32.whl", hash = "sha256:cc27a65069bcabac4552f34fd2dce923ce3fcde0721a16e4fb1b466d63ec831f"}, + {file = "watchfiles-1.0.4-cp310-cp310-win_amd64.whl", hash = "sha256:8b1f135238e75d075359cf506b27bf3f4ca12029c47d3e769d8593a2024ce161"}, + {file = "watchfiles-1.0.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2a9f93f8439639dc244c4d2902abe35b0279102bca7bbcf119af964f51d53c19"}, + {file = "watchfiles-1.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9eea33ad8c418847dd296e61eb683cae1c63329b6d854aefcd412e12d94ee235"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31f1a379c9dcbb3f09cf6be1b7e83b67c0e9faabed0471556d9438a4a4e14202"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab594e75644421ae0a2484554832ca5895f8cab5ab62de30a1a57db460ce06c6"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc2eb5d14a8e0d5df7b36288979176fbb39672d45184fc4b1c004d7c3ce29317"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f68d8e9d5a321163ddacebe97091000955a1b74cd43724e346056030b0bacee"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9ce064e81fe79faa925ff03b9f4c1a98b0bbb4a1b8c1b015afa93030cb21a49"}, + {file = "watchfiles-1.0.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b77d5622ac5cc91d21ae9c2b284b5d5c51085a0bdb7b518dba263d0af006132c"}, + {file = "watchfiles-1.0.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1941b4e39de9b38b868a69b911df5e89dc43767feeda667b40ae032522b9b5f1"}, + {file = "watchfiles-1.0.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f8c4998506241dedf59613082d1c18b836e26ef2a4caecad0ec41e2a15e4226"}, + {file = "watchfiles-1.0.4-cp311-cp311-win32.whl", hash = "sha256:4ebbeca9360c830766b9f0df3640b791be569d988f4be6c06d6fae41f187f105"}, + {file = "watchfiles-1.0.4-cp311-cp311-win_amd64.whl", hash = "sha256:05d341c71f3d7098920f8551d4df47f7b57ac5b8dad56558064c3431bdfc0b74"}, + {file = "watchfiles-1.0.4-cp311-cp311-win_arm64.whl", hash = "sha256:32b026a6ab64245b584acf4931fe21842374da82372d5c039cba6bf99ef722f3"}, + {file = "watchfiles-1.0.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:229e6ec880eca20e0ba2f7e2249c85bae1999d330161f45c78d160832e026ee2"}, + {file = "watchfiles-1.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5717021b199e8353782dce03bd8a8f64438832b84e2885c4a645f9723bf656d9"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0799ae68dfa95136dde7c472525700bd48777875a4abb2ee454e3ab18e9fc712"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43b168bba889886b62edb0397cab5b6490ffb656ee2fcb22dec8bfeb371a9e12"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb2c46e275fbb9f0c92e7654b231543c7bbfa1df07cdc4b99fa73bedfde5c844"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:857f5fc3aa027ff5e57047da93f96e908a35fe602d24f5e5d8ce64bf1f2fc733"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55ccfd27c497b228581e2838d4386301227fc0cb47f5a12923ec2fe4f97b95af"}, + {file = "watchfiles-1.0.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c11ea22304d17d4385067588123658e9f23159225a27b983f343fcffc3e796a"}, + {file = "watchfiles-1.0.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:74cb3ca19a740be4caa18f238298b9d472c850f7b2ed89f396c00a4c97e2d9ff"}, + {file = "watchfiles-1.0.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7cce76c138a91e720d1df54014a047e680b652336e1b73b8e3ff3158e05061e"}, + {file = "watchfiles-1.0.4-cp312-cp312-win32.whl", hash = "sha256:b045c800d55bc7e2cadd47f45a97c7b29f70f08a7c2fa13241905010a5493f94"}, + {file = "watchfiles-1.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:c2acfa49dd0ad0bf2a9c0bb9a985af02e89345a7189be1efc6baa085e0f72d7c"}, + {file = "watchfiles-1.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:22bb55a7c9e564e763ea06c7acea24fc5d2ee5dfc5dafc5cfbedfe58505e9f90"}, + {file = "watchfiles-1.0.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:8012bd820c380c3d3db8435e8cf7592260257b378b649154a7948a663b5f84e9"}, + {file = "watchfiles-1.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa216f87594f951c17511efe5912808dfcc4befa464ab17c98d387830ce07b60"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c9953cf85529c05b24705639ffa390f78c26449e15ec34d5339e8108c7c407"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7cf684aa9bba4cd95ecb62c822a56de54e3ae0598c1a7f2065d51e24637a3c5d"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f44a39aee3cbb9b825285ff979ab887a25c5d336e5ec3574f1506a4671556a8d"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a38320582736922be8c865d46520c043bff350956dfc9fbaee3b2df4e1740a4b"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39f4914548b818540ef21fd22447a63e7be6e24b43a70f7642d21f1e73371590"}, + {file = "watchfiles-1.0.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f12969a3765909cf5dc1e50b2436eb2c0e676a3c75773ab8cc3aa6175c16e902"}, + {file = "watchfiles-1.0.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:0986902677a1a5e6212d0c49b319aad9cc48da4bd967f86a11bde96ad9676ca1"}, + {file = "watchfiles-1.0.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:308ac265c56f936636e3b0e3f59e059a40003c655228c131e1ad439957592303"}, + {file = "watchfiles-1.0.4-cp313-cp313-win32.whl", hash = "sha256:aee397456a29b492c20fda2d8961e1ffb266223625346ace14e4b6d861ba9c80"}, + {file = "watchfiles-1.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:d6097538b0ae5c1b88c3b55afa245a66793a8fec7ada6755322e465fb1a0e8cc"}, + {file = "watchfiles-1.0.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:d3452c1ec703aa1c61e15dfe9d482543e4145e7c45a6b8566978fbb044265a21"}, + {file = "watchfiles-1.0.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7b75fee5a16826cf5c46fe1c63116e4a156924d668c38b013e6276f2582230f0"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e997802d78cdb02623b5941830ab06f8860038faf344f0d288d325cc9c5d2ff"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0611d244ce94d83f5b9aff441ad196c6e21b55f77f3c47608dcf651efe54c4a"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9745a4210b59e218ce64c91deb599ae8775c8a9da4e95fb2ee6fe745fc87d01a"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4810ea2ae622add560f4aa50c92fef975e475f7ac4900ce5ff5547b2434642d8"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:740d103cd01458f22462dedeb5a3382b7f2c57d07ff033fbc9465919e5e1d0f3"}, + {file = "watchfiles-1.0.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbd912a61543a36aef85e34f212e5d2486e7c53ebfdb70d1e0b060cc50dd0bf"}, + {file = "watchfiles-1.0.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0bc80d91ddaf95f70258cf78c471246846c1986bcc5fd33ccc4a1a67fcb40f9a"}, + {file = "watchfiles-1.0.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab0311bb2ffcd9f74b6c9de2dda1612c13c84b996d032cd74799adb656af4e8b"}, + {file = "watchfiles-1.0.4-cp39-cp39-win32.whl", hash = "sha256:02a526ee5b5a09e8168314c905fc545c9bc46509896ed282aeb5a8ba9bd6ca27"}, + {file = "watchfiles-1.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:a5ae5706058b27c74bac987d615105da17724172d5aaacc6c362a40599b6de43"}, + {file = "watchfiles-1.0.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdcc92daeae268de1acf5b7befcd6cfffd9a047098199056c72e4623f531de18"}, + {file = "watchfiles-1.0.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d8d3d9203705b5797f0af7e7e5baa17c8588030aaadb7f6a86107b7247303817"}, + {file = "watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdef5a1be32d0b07dcea3318a0be95d42c98ece24177820226b56276e06b63b0"}, + {file = "watchfiles-1.0.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:342622287b5604ddf0ed2d085f3a589099c9ae8b7331df3ae9845571586c4f3d"}, + {file = "watchfiles-1.0.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9fe37a2de80aa785d340f2980276b17ef697ab8db6019b07ee4fd28a8359d2f3"}, + {file = "watchfiles-1.0.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9d1ef56b56ed7e8f312c934436dea93bfa3e7368adfcf3df4c0da6d4de959a1e"}, + {file = "watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95b42cac65beae3a362629950c444077d1b44f1790ea2772beaea95451c086bb"}, + {file = "watchfiles-1.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e0227b8ed9074c6172cf55d85b5670199c99ab11fd27d2c473aa30aec67ee42"}, + {file = "watchfiles-1.0.4.tar.gz", hash = "sha256:6ba473efd11062d73e4f00c2b730255f9c1bdd73cd5f9fe5b5da8dbd4a717205"}, +] + +[package.dependencies] +anyio = ">=3.0.0" [[package]] name = "webencodings" version = "0.5.1" description = "Character encoding aliases for legacy web content" -category = "main" optional = false python-versions = "*" +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] [[package]] name = "werkzeug" -version = "2.0.2" +version = "3.0.3" description = "The comprehensive WSGI web application library." -category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +files = [ + {file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"}, + {file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" [package.extras] -watchdog = ["watchdog"] +watchdog = ["watchdog (>=2.3)"] + +[[package]] +name = "wrapt" +version = "1.16.0" +description = "Module for decorators, wrappers and monkey patching." +optional = false +python-versions = ">=3.6" +files = [ + {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, + {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, + {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, + {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, + {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, + {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, + {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, + {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, + {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, + {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, + {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, + {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, + {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, + {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, + {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, + {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, + {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, + {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, + {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, + {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, + {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, + {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, + {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, + {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, + {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, + {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, + {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, + {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, + {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, + {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, + {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, + {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, + {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, + {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, + {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, + {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, + {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, +] [[package]] name = "wsproto" -version = "1.0.0" +version = "1.2.0" description = "WebSockets state-machine based protocol implementation" -category = "main" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.7.0" +files = [ + {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, + {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, +] [package.dependencies] h11 = ">=0.9.0,<1" [[package]] name = "zipp" -version = "3.7.0" +version = "3.20.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "zipp-3.20.0-py3-none-any.whl", hash = "sha256:58da6168be89f0be59beb194da1250516fdaa062ccebd30127ac65d30045e10d"}, + {file = "zipp-3.20.0.tar.gz", hash = "sha256:0145e43d89664cfe1a2e533adc75adafed82fe2da404b4bbb6b026c0157bdb31"}, +] [package.extras] -docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [metadata] -lock-version = "1.1" -python-versions = ">=3.9,<3.11" -content-hash = "1f6a0dd3780d8857ba0d5123814f299a8178a80e79c2235805623f43b8e0381f" - -[metadata.files] -aiofiles = [ - {file = "aiofiles-0.7.0-py3-none-any.whl", hash = "sha256:c67a6823b5f23fcab0a2595a289cec7d8c863ffcb4322fb8cd6b90400aedfdbc"}, - {file = "aiofiles-0.7.0.tar.gz", hash = "sha256:a1c4fc9b2ff81568c83e21392a82f344ea9d23da906e4f6a52662764545e19d4"}, -] -alembic = [ - {file = "alembic-1.7.6-py3-none-any.whl", hash = "sha256:ad842f2c3ab5c5d4861232730779c05e33db4ba880a08b85eb505e87c01095bc"}, - {file = "alembic-1.7.6.tar.gz", hash = "sha256:6c0c05e9768a896d804387e20b299880fe01bc56484246b0dffe8075d6d3d847"}, -] -anyio = [ - {file = "anyio-3.5.0-py3-none-any.whl", hash = "sha256:b5fa16c5ff93fa1046f2eeb5bbff2dad4d3514d6cda61d02816dba34fa8c3c2e"}, - {file = "anyio-3.5.0.tar.gz", hash = "sha256:a0aeffe2fb1fdf374a8e4b471444f0f3ac4fb9f5a5b542b48824475e0042a5a6"}, -] -asgiref = [ - {file = "asgiref-3.5.0-py3-none-any.whl", hash = "sha256:88d59c13d634dcffe0510be048210188edd79aeccb6a6c9028cdad6f31d730a9"}, - {file = "asgiref-3.5.0.tar.gz", hash = "sha256:2f8abc20f7248433085eda803936d98992f1343ddb022065779f37c5da0181d0"}, -] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] -attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, -] -authlib = [ - {file = "Authlib-0.15.5-py2.py3-none-any.whl", hash = "sha256:ecf4a7a9f2508c0bb07e93a752dd3c495cfaffc20e864ef0ffc95e3f40d2abaf"}, - {file = "Authlib-0.15.5.tar.gz", hash = "sha256:b83cf6360c8e92b0e9df0d1f32d675790bcc4e3c03977499b1eed24dcdef4252"}, -] -bcrypt = [ - {file = "bcrypt-3.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c95d4cbebffafcdd28bd28bb4e25b31c50f6da605c81ffd9ad8a3d1b2ab7b1b6"}, - {file = "bcrypt-3.2.0-cp36-abi3-manylinux1_x86_64.whl", hash = "sha256:63d4e3ff96188e5898779b6057878fecf3f11cfe6ec3b313ea09955d587ec7a7"}, - {file = "bcrypt-3.2.0-cp36-abi3-manylinux2010_x86_64.whl", hash = "sha256:cd1ea2ff3038509ea95f687256c46b79f5fc382ad0aa3664d200047546d511d1"}, - {file = "bcrypt-3.2.0-cp36-abi3-manylinux2014_aarch64.whl", hash = "sha256:cdcdcb3972027f83fe24a48b1e90ea4b584d35f1cc279d76de6fc4b13376239d"}, - {file = "bcrypt-3.2.0-cp36-abi3-win32.whl", hash = "sha256:a67fb841b35c28a59cebed05fbd3e80eea26e6d75851f0574a9273c80f3e9b55"}, - {file = "bcrypt-3.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:81fec756feff5b6818ea7ab031205e1d323d8943d237303baca2c5f9c7846f34"}, - {file = "bcrypt-3.2.0.tar.gz", hash = "sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29"}, -] -bleach = [ - {file = "bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"}, - {file = "bleach-4.1.0.tar.gz", hash = "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da"}, -] -certifi = [ - {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"}, - {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"}, -] -cffi = [ - {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, - {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, - {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, - {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, - {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, - {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, - {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, - {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, - {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, - {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, - {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, - {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, - {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, - {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, - {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, - {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, - {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, - {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, - {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, - {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, - {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, - {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, - {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, - {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, - {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, - {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, - {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, - {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, - {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, - {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, -] -charset-normalizer = [ - {file = "charset-normalizer-2.0.11.tar.gz", hash = "sha256:98398a9d69ee80548c762ba991a4728bfc3836768ed226b3945908d1a688371c"}, - {file = "charset_normalizer-2.0.11-py3-none-any.whl", hash = "sha256:2842d8f5e82a1f6aa437380934d5e1cd4fcf2003b06fed6940769c164a480a45"}, -] -click = [ - {file = "click-8.0.3-py3-none-any.whl", hash = "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3"}, - {file = "click-8.0.3.tar.gz", hash = "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b"}, -] -colorama = [ - {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, - {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, -] -coverage = [ - {file = "coverage-6.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeffd96882d8c06d31b65dddcf51db7c612547babc1c4c5db6a011abe9798525"}, - {file = "coverage-6.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:621f6ea7260ea2ffdaec64fe5cb521669984f567b66f62f81445221d4754df4c"}, - {file = "coverage-6.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84f2436d6742c01136dd940ee158bfc7cf5ced3da7e4c949662b8703b5cd8145"}, - {file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de73fca6fb403dd72d4da517cfc49fcf791f74eee697d3219f6be29adf5af6ce"}, - {file = "coverage-6.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78fbb2be068a13a5d99dce9e1e7d168db880870f7bc73f876152130575bd6167"}, - {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f5a4551dfd09c3bd12fca8144d47fe7745275adf3229b7223c2f9e29a975ebda"}, - {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7bff3a98f63b47464480de1b5bdd80c8fade0ba2832c9381253c9b74c4153c27"}, - {file = "coverage-6.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a06c358f4aed05fa1099c39decc8022261bb07dfadc127c08cfbd1391b09689e"}, - {file = "coverage-6.3.1-cp310-cp310-win32.whl", hash = "sha256:9fff3ff052922cb99f9e52f63f985d4f7a54f6b94287463bc66b7cdf3eb41217"}, - {file = "coverage-6.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:276b13cc085474e482566c477c25ed66a097b44c6e77132f3304ac0b039f83eb"}, - {file = "coverage-6.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:56c4a409381ddd7bbff134e9756077860d4e8a583d310a6f38a2315b9ce301d0"}, - {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9eb494070aa060ceba6e4bbf44c1bc5fa97bfb883a0d9b0c9049415f9e944793"}, - {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e15d424b8153756b7c903bde6d4610be0c3daca3986173c18dd5c1a1625e4cd"}, - {file = "coverage-6.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d47a897c1e91f33f177c21de897267b38fbb45f2cd8e22a710bcef1df09ac1"}, - {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:25e73d4c81efa8ea3785274a2f7f3bfbbeccb6fcba2a0bdd3be9223371c37554"}, - {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fac0bcc5b7e8169bffa87f0dcc24435446d329cbc2b5486d155c2e0f3b493ae1"}, - {file = "coverage-6.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:72128176fea72012063200b7b395ed8a57849282b207321124d7ff14e26988e8"}, - {file = "coverage-6.3.1-cp37-cp37m-win32.whl", hash = "sha256:1bc6d709939ff262fd1432f03f080c5042dc6508b6e0d3d20e61dd045456a1a0"}, - {file = "coverage-6.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:618eeba986cea7f621d8607ee378ecc8c2504b98b3fdc4952b30fe3578304687"}, - {file = "coverage-6.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ed164af5c9078596cfc40b078c3b337911190d3faeac830c3f1274f26b8320"}, - {file = "coverage-6.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:352c68e233409c31048a3725c446a9e48bbff36e39db92774d4f2380d630d8f8"}, - {file = "coverage-6.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:448d7bde7ceb6c69e08474c2ddbc5b4cd13c9e4aa4a717467f716b5fc938a734"}, - {file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fde6b90889522c220dd56a670102ceef24955d994ff7af2cb786b4ba8fe11e4"}, - {file = "coverage-6.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e647a0be741edbb529a72644e999acb09f2ad60465f80757da183528941ff975"}, - {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a5cdc3adb4f8bb8d8f5e64c2e9e282bc12980ef055ec6da59db562ee9bdfefa"}, - {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2dd70a167843b4b4b2630c0c56f1b586fe965b4f8ac5da05b6690344fd065c6b"}, - {file = "coverage-6.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9ad0a117b8dc2061ce9461ea4c1b4799e55edceb236522c5b8f958ce9ed8fa9a"}, - {file = "coverage-6.3.1-cp38-cp38-win32.whl", hash = "sha256:e92c7a5f7d62edff50f60a045dc9542bf939758c95b2fcd686175dd10ce0ed10"}, - {file = "coverage-6.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:482fb42eea6164894ff82abbcf33d526362de5d1a7ed25af7ecbdddd28fc124f"}, - {file = "coverage-6.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c5b81fb37db76ebea79aa963b76d96ff854e7662921ce742293463635a87a78d"}, - {file = "coverage-6.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a4f923b9ab265136e57cc14794a15b9dcea07a9c578609cd5dbbfff28a0d15e6"}, - {file = "coverage-6.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56d296cbc8254a7dffdd7bcc2eb70be5a233aae7c01856d2d936f5ac4e8ac1f1"}, - {file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245ab82e8554fa88c4b2ab1e098ae051faac5af829efdcf2ce6b34dccd5567c"}, - {file = "coverage-6.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f2b05757c92ad96b33dbf8e8ec8d4ccb9af6ae3c9e9bd141c7cc44d20c6bcba"}, - {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9e3dd806f34de38d4c01416344e98eab2437ac450b3ae39c62a0ede2f8b5e4ed"}, - {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d651fde74a4d3122e5562705824507e2f5b2d3d57557f1916c4b27635f8fbe3f"}, - {file = "coverage-6.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:704f89b87c4f4737da2860695a18c852b78ec7279b24eedacab10b29067d3a38"}, - {file = "coverage-6.3.1-cp39-cp39-win32.whl", hash = "sha256:2aed4761809640f02e44e16b8b32c1a5dee5e80ea30a0ff0912158bde9c501f2"}, - {file = "coverage-6.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:9976fb0a5709988778ac9bc44f3d50fccd989987876dfd7716dee28beed0a9fa"}, - {file = "coverage-6.3.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:463e52616ea687fd323888e86bf25e864a3cc6335a043fad6bbb037dbf49bbe2"}, - {file = "coverage-6.3.1.tar.gz", hash = "sha256:6c3f6158b02ac403868eea390930ae64e9a9a2a5bbfafefbb920d29258d9f2f8"}, -] -cryptography = [ - {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:73bc2d3f2444bcfeac67dd130ff2ea598ea5f20b40e36d19821b4df8c9c5037b"}, - {file = "cryptography-36.0.1-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:2d87cdcb378d3cfed944dac30596da1968f88fb96d7fc34fdae30a99054b2e31"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74d6c7e80609c0f4c2434b97b80c7f8fdfaa072ca4baab7e239a15d6d70ed73a"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:6c0c021f35b421ebf5976abf2daacc47e235f8b6082d3396a2fe3ccd537ab173"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59a9d55027a8b88fd9fd2826c4392bd487d74bf628bb9d39beecc62a644c12"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a817b961b46894c5ca8a66b599c745b9a3d9f822725221f0e0fe49dc043a3a3"}, - {file = "cryptography-36.0.1-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:94ae132f0e40fe48f310bba63f477f14a43116f05ddb69d6fa31e93f05848ae2"}, - {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7be0eec337359c155df191d6ae00a5e8bbb63933883f4f5dffc439dac5348c3f"}, - {file = "cryptography-36.0.1-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:e0344c14c9cb89e76eb6a060e67980c9e35b3f36691e15e1b7a9e58a0a6c6dc3"}, - {file = "cryptography-36.0.1-cp36-abi3-win32.whl", hash = "sha256:4caa4b893d8fad33cf1964d3e51842cd78ba87401ab1d2e44556826df849a8ca"}, - {file = "cryptography-36.0.1-cp36-abi3-win_amd64.whl", hash = "sha256:391432971a66cfaf94b21c24ab465a4cc3e8bf4a939c1ca5c3e3a6e0abebdbcf"}, - {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:bb5829d027ff82aa872d76158919045a7c1e91fbf241aec32cb07956e9ebd3c9"}, - {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc15b1c22e55c4d5566e3ca4db8689470a0ca2babef8e3a9ee057a8b82ce4b1"}, - {file = "cryptography-36.0.1-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:596f3cd67e1b950bc372c33f1a28a0692080625592ea6392987dba7f09f17a94"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:30ee1eb3ebe1644d1c3f183d115a8c04e4e603ed6ce8e394ed39eea4a98469ac"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec63da4e7e4a5f924b90af42eddf20b698a70e58d86a72d943857c4c6045b3ee"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca238ceb7ba0bdf6ce88c1b74a87bffcee5afbfa1e41e173b1ceb095b39add46"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:ca28641954f767f9822c24e927ad894d45d5a1e501767599647259cbf030b903"}, - {file = "cryptography-36.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:39bdf8e70eee6b1c7b289ec6e5d84d49a6bfa11f8b8646b5b3dfe41219153316"}, - {file = "cryptography-36.0.1.tar.gz", hash = "sha256:53e5c1dc3d7a953de055d77bef2ff607ceef7a2aac0353b5d630ab67f7423638"}, -] -dnspython = [ - {file = "dnspython-2.2.0-py3-none-any.whl", hash = "sha256:081649da27ced5e75709a1ee542136eaba9842a0fe4c03da4fb0a3d3ed1f3c44"}, - {file = "dnspython-2.2.0.tar.gz", hash = "sha256:e79351e032d0b606b98d38a4b0e6e2275b31a5b85c873e587cc11b73aca026d6"}, -] -dunamai = [ - {file = "dunamai-1.8.0-py3-none-any.whl", hash = "sha256:846855e45d5969f6d11835d486bbf4d6ca175d4169a0ab11f619a5135cc86bdf"}, - {file = "dunamai-1.8.0.tar.gz", hash = "sha256:ff1f958af3575ec612e72c84bf96367469f418d31b9685f8311a5de2eb754a85"}, -] -email-validator = [ - {file = "email_validator-1.1.3-py2.py3-none-any.whl", hash = "sha256:5675c8ceb7106a37e40e2698a57c056756bf3f272cfa8682a4f87ebd95d8440b"}, - {file = "email_validator-1.1.3.tar.gz", hash = "sha256:aa237a65f6f4da067119b7df3f13e89c25c051327b2b5b66dc075f33d62480d7"}, -] -execnet = [ - {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, - {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, -] -fakeredis = [ - {file = "fakeredis-1.7.0-py3-none-any.whl", hash = "sha256:6f1e04f64557ad3b6835bdc6e5a8d022cbace4bdc24a47ad58f6a72e0fbff760"}, - {file = "fakeredis-1.7.0.tar.gz", hash = "sha256:c9bd12e430336cbd3e189fae0e91eb99997b93e76dbfdd6ed67fa352dc684c71"}, -] -fastapi = [ - {file = "fastapi-0.71.0-py3-none-any.whl", hash = "sha256:a78eca6b084de9667f2d5f37e2ae297270e5a119cd01c2f04815795da92fc87f"}, - {file = "fastapi-0.71.0.tar.gz", hash = "sha256:2b5ac0ae89c80b40d1dd4b2ea0bb1f78d7c4affd3644d080bf050f084759fff2"}, -] -feedgen = [ - {file = "feedgen-0.9.0.tar.gz", hash = "sha256:8e811bdbbed6570034950db23a4388453628a70e689a6e8303ccec430f5a804a"}, -] -filelock = [ - {file = "filelock-3.4.2-py3-none-any.whl", hash = "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146"}, - {file = "filelock-3.4.2.tar.gz", hash = "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80"}, -] -flake8 = [ - {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, - {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, -] -greenlet = [ - {file = "greenlet-1.1.2-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6"}, - {file = "greenlet-1.1.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a"}, - {file = "greenlet-1.1.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d"}, - {file = "greenlet-1.1.2-cp27-cp27m-win32.whl", hash = "sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713"}, - {file = "greenlet-1.1.2-cp27-cp27m-win_amd64.whl", hash = "sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40"}, - {file = "greenlet-1.1.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d"}, - {file = "greenlet-1.1.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8"}, - {file = "greenlet-1.1.2-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d"}, - {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497"}, - {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1"}, - {file = "greenlet-1.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58"}, - {file = "greenlet-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708"}, - {file = "greenlet-1.1.2-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23"}, - {file = "greenlet-1.1.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee"}, - {file = "greenlet-1.1.2-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c"}, - {file = "greenlet-1.1.2-cp35-cp35m-win32.whl", hash = "sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963"}, - {file = "greenlet-1.1.2-cp35-cp35m-win_amd64.whl", hash = "sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e"}, - {file = "greenlet-1.1.2-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073"}, - {file = "greenlet-1.1.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c"}, - {file = "greenlet-1.1.2-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e"}, - {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce"}, - {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08"}, - {file = "greenlet-1.1.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168"}, - {file = "greenlet-1.1.2-cp36-cp36m-win32.whl", hash = "sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa"}, - {file = "greenlet-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d"}, - {file = "greenlet-1.1.2-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4"}, - {file = "greenlet-1.1.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"}, - {file = "greenlet-1.1.2-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c"}, - {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1"}, - {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28"}, - {file = "greenlet-1.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5"}, - {file = "greenlet-1.1.2-cp37-cp37m-win32.whl", hash = "sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc"}, - {file = "greenlet-1.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06"}, - {file = "greenlet-1.1.2-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0"}, - {file = "greenlet-1.1.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627"}, - {file = "greenlet-1.1.2-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478"}, - {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43"}, - {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711"}, - {file = "greenlet-1.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b"}, - {file = "greenlet-1.1.2-cp38-cp38-win32.whl", hash = "sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd"}, - {file = "greenlet-1.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3"}, - {file = "greenlet-1.1.2-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67"}, - {file = "greenlet-1.1.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab"}, - {file = "greenlet-1.1.2-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5"}, - {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88"}, - {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b"}, - {file = "greenlet-1.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3"}, - {file = "greenlet-1.1.2-cp39-cp39-win32.whl", hash = "sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf"}, - {file = "greenlet-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd"}, - {file = "greenlet-1.1.2.tar.gz", hash = "sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a"}, -] -gunicorn = [ - {file = "gunicorn-20.1.0-py3-none-any.whl", hash = "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e"}, - {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, -] -h11 = [ - {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, - {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, -] -h2 = [ - {file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"}, - {file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"}, -] -hpack = [ - {file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"}, - {file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"}, -] -httpcore = [ - {file = "httpcore-0.13.7-py3-none-any.whl", hash = "sha256:369aa481b014cf046f7067fddd67d00560f2f00426e79569d99cb11245134af0"}, - {file = "httpcore-0.13.7.tar.gz", hash = "sha256:036f960468759e633574d7c121afba48af6419615d36ab8ede979f1ad6276fa3"}, -] -httpx = [ - {file = "httpx-0.20.0-py3-none-any.whl", hash = "sha256:33af5aad9bdc82ef1fc89219c1e36f5693bf9cd0ebe330884df563445682c0f8"}, - {file = "httpx-0.20.0.tar.gz", hash = "sha256:09606d630f070d07f9ff28104fbcea429ea0014c1e89ac90b4d8de8286c40e7b"}, -] -hypercorn = [ - {file = "Hypercorn-0.11.2-py3-none-any.whl", hash = "sha256:8007c10f81566920f8ae12c0e26e146f94ca70506da964b5a727ad610aa1d821"}, - {file = "Hypercorn-0.11.2.tar.gz", hash = "sha256:5ba1e719c521080abd698ff5781a2331e34ef50fc1c89a50960538115a896a9a"}, -] -hyperframe = [ - {file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"}, - {file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"}, -] -idna = [ - {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, - {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, -] -importlib-metadata = [ - {file = "importlib_metadata-4.10.1-py3-none-any.whl", hash = "sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6"}, - {file = "importlib_metadata-4.10.1.tar.gz", hash = "sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e"}, -] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -isort = [ - {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, - {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, -] -itsdangerous = [ - {file = "itsdangerous-2.0.1-py3-none-any.whl", hash = "sha256:5174094b9637652bdb841a3029700391451bd092ba3db90600dea710ba28e97c"}, - {file = "itsdangerous-2.0.1.tar.gz", hash = "sha256:9e724d68fc22902a1435351f84c3fb8623f303fffcc566a4cb952df8c572cff0"}, -] -jinja2 = [ - {file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"}, - {file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"}, -] -lxml = [ - {file = "lxml-4.7.1-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:d546431636edb1d6a608b348dd58cc9841b81f4116745857b6cb9f8dadb2725f"}, - {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6308062534323f0d3edb4e702a0e26a76ca9e0e23ff99be5d82750772df32a9e"}, - {file = "lxml-4.7.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f76dbe44e31abf516114f6347a46fa4e7c2e8bceaa4b6f7ee3a0a03c8eba3c17"}, - {file = "lxml-4.7.1-cp27-cp27m-win32.whl", hash = "sha256:d5618d49de6ba63fe4510bdada62d06a8acfca0b4b5c904956c777d28382b419"}, - {file = "lxml-4.7.1-cp27-cp27m-win_amd64.whl", hash = "sha256:9393a05b126a7e187f3e38758255e0edf948a65b22c377414002d488221fdaa2"}, - {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:50d3dba341f1e583265c1a808e897b4159208d814ab07530202b6036a4d86da5"}, - {file = "lxml-4.7.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:44f552e0da3c8ee3c28e2eb82b0b784200631687fc6a71277ea8ab0828780e7d"}, - {file = "lxml-4.7.1-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:e662c6266e3a275bdcb6bb049edc7cd77d0b0f7e119a53101d367c841afc66dc"}, - {file = "lxml-4.7.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:4c093c571bc3da9ebcd484e001ba18b8452903cd428c0bc926d9b0141bcb710e"}, - {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3e26ad9bc48d610bf6cc76c506b9e5ad9360ed7a945d9be3b5b2c8535a0145e3"}, - {file = "lxml-4.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a5f623aeaa24f71fce3177d7fee875371345eb9102b355b882243e33e04b7175"}, - {file = "lxml-4.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7b5e2acefd33c259c4a2e157119c4373c8773cf6793e225006a1649672ab47a6"}, - {file = "lxml-4.7.1-cp310-cp310-win32.whl", hash = "sha256:67fa5f028e8a01e1d7944a9fb616d1d0510d5d38b0c41708310bd1bc45ae89f6"}, - {file = "lxml-4.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:b1d381f58fcc3e63fcc0ea4f0a38335163883267f77e4c6e22d7a30877218a0e"}, - {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:38d9759733aa04fb1697d717bfabbedb21398046bd07734be7cccc3d19ea8675"}, - {file = "lxml-4.7.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:dfd0d464f3d86a1460683cd742306d1138b4e99b79094f4e07e1ca85ee267fe7"}, - {file = "lxml-4.7.1-cp35-cp35m-win32.whl", hash = "sha256:534e946bce61fd162af02bad7bfd2daec1521b71d27238869c23a672146c34a5"}, - {file = "lxml-4.7.1-cp35-cp35m-win_amd64.whl", hash = "sha256:6ec829058785d028f467be70cd195cd0aaf1a763e4d09822584ede8c9eaa4b03"}, - {file = "lxml-4.7.1-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:ade74f5e3a0fd17df5782896ddca7ddb998845a5f7cd4b0be771e1ffc3b9aa5b"}, - {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:41358bfd24425c1673f184d7c26c6ae91943fe51dfecc3603b5e08187b4bcc55"}, - {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6e56521538f19c4a6690f439fefed551f0b296bd785adc67c1777c348beb943d"}, - {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5b0f782f0e03555c55e37d93d7a57454efe7495dab33ba0ccd2dbe25fc50f05d"}, - {file = "lxml-4.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:490712b91c65988012e866c411a40cc65b595929ececf75eeb4c79fcc3bc80a6"}, - {file = "lxml-4.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:34c22eb8c819d59cec4444d9eebe2e38b95d3dcdafe08965853f8799fd71161d"}, - {file = "lxml-4.7.1-cp36-cp36m-win32.whl", hash = "sha256:2a906c3890da6a63224d551c2967413b8790a6357a80bf6b257c9a7978c2c42d"}, - {file = "lxml-4.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:36b16fecb10246e599f178dd74f313cbdc9f41c56e77d52100d1361eed24f51a"}, - {file = "lxml-4.7.1-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:a5edc58d631170de90e50adc2cc0248083541affef82f8cd93bea458e4d96db8"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:87c1b0496e8c87ec9db5383e30042357b4839b46c2d556abd49ec770ce2ad868"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:0a5f0e4747f31cff87d1eb32a6000bde1e603107f632ef4666be0dc065889c7a"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bf6005708fc2e2c89a083f258b97709559a95f9a7a03e59f805dd23c93bc3986"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc15874816b9320581133ddc2096b644582ab870cf6a6ed63684433e7af4b0d3"}, - {file = "lxml-4.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:0b5e96e25e70917b28a5391c2ed3ffc6156513d3db0e1476c5253fcd50f7a944"}, - {file = "lxml-4.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ec9027d0beb785a35aa9951d14e06d48cfbf876d8ff67519403a2522b181943b"}, - {file = "lxml-4.7.1-cp37-cp37m-win32.whl", hash = "sha256:9fbc0dee7ff5f15c4428775e6fa3ed20003140560ffa22b88326669d53b3c0f4"}, - {file = "lxml-4.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:1104a8d47967a414a436007c52f533e933e5d52574cab407b1e49a4e9b5ddbd1"}, - {file = "lxml-4.7.1-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:fc9fb11b65e7bc49f7f75aaba1b700f7181d95d4e151cf2f24d51bfd14410b77"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:317bd63870b4d875af3c1be1b19202de34c32623609ec803b81c99193a788c1e"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:610807cea990fd545b1559466971649e69302c8a9472cefe1d6d48a1dee97440"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:09b738360af8cb2da275998a8bf79517a71225b0de41ab47339c2beebfff025f"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a2ab9d089324d77bb81745b01f4aeffe4094306d939e92ba5e71e9a6b99b71e"}, - {file = "lxml-4.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:eed394099a7792834f0cb4a8f615319152b9d801444c1c9e1b1a2c36d2239f9e"}, - {file = "lxml-4.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:735e3b4ce9c0616e85f302f109bdc6e425ba1670a73f962c9f6b98a6d51b77c9"}, - {file = "lxml-4.7.1-cp38-cp38-win32.whl", hash = "sha256:772057fba283c095db8c8ecde4634717a35c47061d24f889468dc67190327bcd"}, - {file = "lxml-4.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:13dbb5c7e8f3b6a2cf6e10b0948cacb2f4c9eb05029fe31c60592d08ac63180d"}, - {file = "lxml-4.7.1-cp39-cp39-macosx_10_14_x86_64.whl", hash = "sha256:718d7208b9c2d86aaf0294d9381a6acb0158b5ff0f3515902751404e318e02c9"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:5bee1b0cbfdb87686a7fb0e46f1d8bd34d52d6932c0723a86de1cc532b1aa489"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:e410cf3a2272d0a85526d700782a2fa92c1e304fdcc519ba74ac80b8297adf36"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:585ea241ee4961dc18a95e2f5581dbc26285fcf330e007459688096f76be8c42"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a555e06566c6dc167fbcd0ad507ff05fd9328502aefc963cb0a0547cfe7f00db"}, - {file = "lxml-4.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:adaab25be351fff0d8a691c4f09153647804d09a87a4e4ea2c3f9fe9e8651851"}, - {file = "lxml-4.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:82d16a64236970cb93c8d63ad18c5b9f138a704331e4b916b2737ddfad14e0c4"}, - {file = "lxml-4.7.1-cp39-cp39-win32.whl", hash = "sha256:59e7da839a1238807226f7143c68a479dee09244d1b3cf8c134f2fce777d12d0"}, - {file = "lxml-4.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:a1bbc4efa99ed1310b5009ce7f3a1784698082ed2c1ef3895332f5df9b3b92c2"}, - {file = "lxml-4.7.1-pp37-pypy37_pp73-macosx_10_14_x86_64.whl", hash = "sha256:0607ff0988ad7e173e5ddf7bf55ee65534bd18a5461183c33e8e41a59e89edf4"}, - {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:6c198bfc169419c09b85ab10cb0f572744e686f40d1e7f4ed09061284fc1303f"}, - {file = "lxml-4.7.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a58d78653ae422df6837dd4ca0036610b8cb4962b5cfdbd337b7b24de9e5f98a"}, - {file = "lxml-4.7.1-pp38-pypy38_pp73-macosx_10_14_x86_64.whl", hash = "sha256:e18281a7d80d76b66a9f9e68a98cf7e1d153182772400d9a9ce855264d7d0ce7"}, - {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8e54945dd2eeb50925500957c7c579df3cd07c29db7810b83cf30495d79af267"}, - {file = "lxml-4.7.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:447d5009d6b5447b2f237395d0018901dcc673f7d9f82ba26c1b9f9c3b444b60"}, - {file = "lxml-4.7.1.tar.gz", hash = "sha256:a1613838aa6b89af4ba10a0f3a972836128801ed008078f8c1244e65958f1b24"}, -] -mako = [ - {file = "Mako-1.1.6-py2.py3-none-any.whl", hash = "sha256:afaf8e515d075b22fad7d7b8b30e4a1c90624ff2f3733a06ec125f5a5f043a57"}, - {file = "Mako-1.1.6.tar.gz", hash = "sha256:4e9e345a41924a954251b95b4b28e14a301145b544901332e658907a7464b6b2"}, -] -markdown = [ - {file = "Markdown-3.3.6-py3-none-any.whl", hash = "sha256:9923332318f843411e9932237530df53162e29dc7a4e2b91e35764583c46c9a3"}, - {file = "Markdown-3.3.6.tar.gz", hash = "sha256:76df8ae32294ec39dcf89340382882dfa12975f87f45c3ed1ecdb1e8cefc7006"}, -] -markupsafe = [ - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, - {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -mysql-connector = [ - {file = "mysql-connector-2.2.9.tar.gz", hash = "sha256:1733e6ce52a049243de3264f1fbc22a852cb35458c4ad739ba88189285efdf32"}, -] -mysqlclient = [ - {file = "mysqlclient-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:02c8826e6add9b20f4cb12dcf016485f7b1d6e30356a1204d05431867a1b3947"}, - {file = "mysqlclient-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:b62d23c11c516cedb887377c8807628c1c65d57593b57853186a6ee18b0c6a5b"}, - {file = "mysqlclient-2.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:2c8410f54492a3d2488a6a53e2d85b7e016751a1e7d116e7aea9c763f59f5e8c"}, - {file = "mysqlclient-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:e6279263d5a9feca3e0edbc2b2a52c057375bf301d47da2089c075ff76331d14"}, - {file = "mysqlclient-2.1.0.tar.gz", hash = "sha256:973235686f1b720536d417bf0a0d39b4ab3d5086b2b6ad5e6752393428c02b12"}, -] -orjson = [ - {file = "orjson-3.6.6-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:e4a7cad6c63306318453980d302c7c0b74c0cc290dd1f433bbd7d31a5af90cf1"}, - {file = "orjson-3.6.6-cp310-cp310-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:e533941dca4a0530a876de32e54bf2fd3269cdec3751aebde7bfb5b5eba98e74"}, - {file = "orjson-3.6.6-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:9adf63be386eaa34278967512b83ff8fc4bed036a246391ae236f68d23c47452"}, - {file = "orjson-3.6.6-cp310-cp310-manylinux_2_24_x86_64.whl", hash = "sha256:3b636753ae34d4619b11ea7d664a2f1e87e55e9738e5123e12bcce22acae9d13"}, - {file = "orjson-3.6.6-cp310-none-win_amd64.whl", hash = "sha256:78a10295ed048fd916c6584d6d27c232eae805a43e7c14be56e3745f784f0eb6"}, - {file = "orjson-3.6.6-cp37-cp37m-macosx_10_7_x86_64.whl", hash = "sha256:82b4f9fb2af7799b52932a62eac484083f930d5519560d6f64b24d66a368d03f"}, - {file = "orjson-3.6.6-cp37-cp37m-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:a0033d07309cc7d8b8c4bc5d42f0dd4422b53ceb91dee9f4086bb2afa70b7772"}, - {file = "orjson-3.6.6-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b321f99473116ab7c7c028377372f7b4adba4029aaca19cd567e83898f55579"}, - {file = "orjson-3.6.6-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:b9c98ed94f1688cc11b5c61b8eea39d854a1a2f09f71d8a5af005461b14994ed"}, - {file = "orjson-3.6.6-cp37-cp37m-manylinux_2_24_x86_64.whl", hash = "sha256:00b333a41392bd07a8603c42670547dbedf9b291485d773f90c6470eff435608"}, - {file = "orjson-3.6.6-cp37-none-win_amd64.whl", hash = "sha256:8d4fd3bdee65a81f2b79c50937d4b3c054e1e6bfa3fc72ed018a97c0c7c3d521"}, - {file = "orjson-3.6.6-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:954c9f8547247cd7a8c91094ff39c9fe314b5eaeaec90b7bfb7384a4108f416f"}, - {file = "orjson-3.6.6-cp38-cp38-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:74e5aed657ed0b91ef05d44d6a26d3e3e12ce4d2d71f75df41a477b05878c4a9"}, - {file = "orjson-3.6.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4008a5130e6e9c33abaa95e939e0e755175da10745740aa6968461b2f16830e2"}, - {file = "orjson-3.6.6-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:012761d5f3d186deb4f6238f15e9ea7c1aac6deebc8f5b741ba3b4fafe017460"}, - {file = "orjson-3.6.6-cp38-cp38-manylinux_2_24_x86_64.whl", hash = "sha256:b464546718a940b48d095a98df4c04808bfa6c8706fe751fc3f9390bc2f82643"}, - {file = "orjson-3.6.6-cp38-none-win_amd64.whl", hash = "sha256:f10a800f4e5a4aab52076d4628e9e4dab9370bdd9d8ea254ebfde846b653ab25"}, - {file = "orjson-3.6.6-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:8010d2610cfab721725ef14d578c7071e946bbdae63322d8f7b49061cf3fde8d"}, - {file = "orjson-3.6.6-cp39-cp39-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:8dca67a4855e1e0f9a2ea0386e8db892708522e1171dc0ddf456932288fbae63"}, - {file = "orjson-3.6.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af065d60523139b99bd35b839c7a2d8c5da55df8a8c4402d2eb6cdc07fa7a624"}, - {file = "orjson-3.6.6-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:fa1f389cc9f766ae0cf7ba3533d5089836b01a5ccb3f8d904297f1fcf3d9dc34"}, - {file = "orjson-3.6.6-cp39-cp39-manylinux_2_24_x86_64.whl", hash = "sha256:ec1221ad78f94d27b162a1d35672b62ef86f27f0e4c2b65051edb480cc86b286"}, - {file = "orjson-3.6.6-cp39-none-win_amd64.whl", hash = "sha256:afed2af55eeda1de6b3f1cbc93431981b19d380fcc04f6ed86e74c1913070304"}, - {file = "orjson-3.6.6.tar.gz", hash = "sha256:55dd988400fa7fbe0e31407c683f5aaab013b5bd967167b8fe058186773c4d6c"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -paginate = [ - {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, -] -parse = [ - {file = "parse-1.19.0.tar.gz", hash = "sha256:9ff82852bcb65d139813e2a5197627a94966245c897796760a3a2a8eb66f020b"}, -] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -poetry-dynamic-versioning = [ - {file = "poetry-dynamic-versioning-0.13.1.tar.gz", hash = "sha256:5c0e7b22560db76812057ef95dadad662ecc63eb270145787eabe73da7c222f9"}, - {file = "poetry_dynamic_versioning-0.13.1-py3-none-any.whl", hash = "sha256:6d79f76436c624653fc06eb9bb54fb4f39b1d54362bc366ad2496855711d3a78"}, -] -posix-ipc = [ - {file = "posix_ipc-1.0.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ccb36ba90efec56a1796f1566eee9561f355a4f45babbc4d18ac46fb2d0b246b"}, - {file = "posix_ipc-1.0.5-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:613bf1afe90e84c06255ec1a6f52c9b24062492de66e5f0dbe068adf67fc3454"}, - {file = "posix_ipc-1.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6095bb4faa2bba8b8d0e833b804e0aedc352d5ed921edeb715010cbcd361e038"}, - {file = "posix_ipc-1.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:621918abe7ec68591c5839b0771d163a9809bc232bf413b9a681bf986ab68d4d"}, - {file = "posix_ipc-1.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f71587ad3a50e82583987f62bfd4ac2343ab6a206d1032e3fc560e8d55fe0346"}, - {file = "posix_ipc-1.0.5.tar.gz", hash = "sha256:6cddb1ce2cf4aae383f2a0079c26c69bee257fe2720f372201ef047f8ceb8b97"}, -] -priority = [ - {file = "priority-2.0.0-py3-none-any.whl", hash = "sha256:6f8eefce5f3ad59baf2c080a664037bb4725cd0a790d53d59ab4059288faf6aa"}, - {file = "priority-2.0.0.tar.gz", hash = "sha256:c965d54f1b8d0d0b19479db3924c7c36cf672dbf2aec92d43fbdaf4492ba18c0"}, -] -prometheus-client = [ - {file = "prometheus_client-0.13.1-py3-none-any.whl", hash = "sha256:357a447fd2359b0a1d2e9b311a0c5778c330cfbe186d880ad5a6b39884652316"}, - {file = "prometheus_client-0.13.1.tar.gz", hash = "sha256:ada41b891b79fca5638bd5cfe149efa86512eaa55987893becd2c6d8d0a5dfc5"}, -] -prometheus-fastapi-instrumentator = [ - {file = "prometheus-fastapi-instrumentator-5.7.1.tar.gz", hash = "sha256:5371f1b494e2b00017a02898d854119b4929025d1a203670b07b3f42dd0b5526"}, - {file = "prometheus_fastapi_instrumentator-5.7.1-py3-none-any.whl", hash = "sha256:da40ea0df14b0e95d584769747fba777522a8df6a8c47cec2edf798f1fff49b5"}, -] -protobuf = [ - {file = "protobuf-3.19.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f51d5a9f137f7a2cec2d326a74b6e3fc79d635d69ffe1b036d39fc7d75430d37"}, - {file = "protobuf-3.19.4-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:09297b7972da685ce269ec52af761743714996b4381c085205914c41fcab59fb"}, - {file = "protobuf-3.19.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072fbc78d705d3edc7ccac58a62c4c8e0cec856987da7df8aca86e647be4e35c"}, - {file = "protobuf-3.19.4-cp310-cp310-win32.whl", hash = "sha256:7bb03bc2873a2842e5ebb4801f5c7ff1bfbdf426f85d0172f7644fcda0671ae0"}, - {file = "protobuf-3.19.4-cp310-cp310-win_amd64.whl", hash = "sha256:f358aa33e03b7a84e0d91270a4d4d8f5df6921abe99a377828839e8ed0c04e07"}, - {file = "protobuf-3.19.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1c91ef4110fdd2c590effb5dca8fdbdcb3bf563eece99287019c4204f53d81a4"}, - {file = "protobuf-3.19.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c438268eebb8cf039552897d78f402d734a404f1360592fef55297285f7f953f"}, - {file = "protobuf-3.19.4-cp36-cp36m-win32.whl", hash = "sha256:835a9c949dc193953c319603b2961c5c8f4327957fe23d914ca80d982665e8ee"}, - {file = "protobuf-3.19.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4276cdec4447bd5015453e41bdc0c0c1234eda08420b7c9a18b8d647add51e4b"}, - {file = "protobuf-3.19.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6cbc312be5e71869d9d5ea25147cdf652a6781cf4d906497ca7690b7b9b5df13"}, - {file = "protobuf-3.19.4-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:54a1473077f3b616779ce31f477351a45b4fef8c9fd7892d6d87e287a38df368"}, - {file = "protobuf-3.19.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:435bb78b37fc386f9275a7035fe4fb1364484e38980d0dd91bc834a02c5ec909"}, - {file = "protobuf-3.19.4-cp37-cp37m-win32.whl", hash = "sha256:16f519de1313f1b7139ad70772e7db515b1420d208cb16c6d7858ea989fc64a9"}, - {file = "protobuf-3.19.4-cp37-cp37m-win_amd64.whl", hash = "sha256:cdc076c03381f5c1d9bb1abdcc5503d9ca8b53cf0a9d31a9f6754ec9e6c8af0f"}, - {file = "protobuf-3.19.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:69da7d39e39942bd52848438462674c463e23963a1fdaa84d88df7fbd7e749b2"}, - {file = "protobuf-3.19.4-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:48ed3877fa43e22bcacc852ca76d4775741f9709dd9575881a373bd3e85e54b2"}, - {file = "protobuf-3.19.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd95d1dfb9c4f4563e6093a9aa19d9c186bf98fa54da5252531cc0d3a07977e7"}, - {file = "protobuf-3.19.4-cp38-cp38-win32.whl", hash = "sha256:b38057450a0c566cbd04890a40edf916db890f2818e8682221611d78dc32ae26"}, - {file = "protobuf-3.19.4-cp38-cp38-win_amd64.whl", hash = "sha256:7ca7da9c339ca8890d66958f5462beabd611eca6c958691a8fe6eccbd1eb0c6e"}, - {file = "protobuf-3.19.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:36cecbabbda242915529b8ff364f2263cd4de7c46bbe361418b5ed859677ba58"}, - {file = "protobuf-3.19.4-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:c1068287025f8ea025103e37d62ffd63fec8e9e636246b89c341aeda8a67c934"}, - {file = "protobuf-3.19.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96bd766831596d6014ca88d86dc8fe0fb2e428c0b02432fd9db3943202bf8c5e"}, - {file = "protobuf-3.19.4-cp39-cp39-win32.whl", hash = "sha256:84123274d982b9e248a143dadd1b9815049f4477dc783bf84efe6250eb4b836a"}, - {file = "protobuf-3.19.4-cp39-cp39-win_amd64.whl", hash = "sha256:3112b58aac3bac9c8be2b60a9daf6b558ca3f7681c130dcdd788ade7c9ffbdca"}, - {file = "protobuf-3.19.4-py2.py3-none-any.whl", hash = "sha256:8961c3a78ebfcd000920c9060a262f082f29838682b1f7201889300c1fbe0616"}, - {file = "protobuf-3.19.4.tar.gz", hash = "sha256:9df0c10adf3e83015ced42a9a7bd64e13d06c4cf45c340d2c63020ea04499d0a"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -pyalpm = [ - {file = "pyalpm-0.10.6.tar.gz", hash = "sha256:99e6ec73b8c46bb12466013f228f831ee0d18e8ab664b91a01c2a3c40de07c7f"}, -] -pycodestyle = [ - {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, - {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, -] -pycparser = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] -pydantic = [ - {file = "pydantic-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cb23bcc093697cdea2708baae4f9ba0e972960a835af22560f6ae4e7e47d33f5"}, - {file = "pydantic-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1d5278bd9f0eee04a44c712982343103bba63507480bfd2fc2790fa70cd64cf4"}, - {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab624700dc145aa809e6f3ec93fb8e7d0f99d9023b713f6a953637429b437d37"}, - {file = "pydantic-1.9.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c8d7da6f1c1049eefb718d43d99ad73100c958a5367d30b9321b092771e96c25"}, - {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3c3b035103bd4e2e4a28da9da7ef2fa47b00ee4a9cf4f1a735214c1bcd05e0f6"}, - {file = "pydantic-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3011b975c973819883842c5ab925a4e4298dffccf7782c55ec3580ed17dc464c"}, - {file = "pydantic-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:086254884d10d3ba16da0588604ffdc5aab3f7f09557b998373e885c690dd398"}, - {file = "pydantic-1.9.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:0fe476769acaa7fcddd17cadd172b156b53546ec3614a4d880e5d29ea5fbce65"}, - {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8e9dcf1ac499679aceedac7e7ca6d8641f0193c591a2d090282aaf8e9445a46"}, - {file = "pydantic-1.9.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1e4c28f30e767fd07f2ddc6f74f41f034d1dd6bc526cd59e63a82fe8bb9ef4c"}, - {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:c86229333cabaaa8c51cf971496f10318c4734cf7b641f08af0a6fbf17ca3054"}, - {file = "pydantic-1.9.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:c0727bda6e38144d464daec31dff936a82917f431d9c39c39c60a26567eae3ed"}, - {file = "pydantic-1.9.0-cp36-cp36m-win_amd64.whl", hash = "sha256:dee5ef83a76ac31ab0c78c10bd7d5437bfdb6358c95b91f1ba7ff7b76f9996a1"}, - {file = "pydantic-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9c9bdb3af48e242838f9f6e6127de9be7063aad17b32215ccc36a09c5cf1070"}, - {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ee7e3209db1e468341ef41fe263eb655f67f5c5a76c924044314e139a1103a2"}, - {file = "pydantic-1.9.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b6037175234850ffd094ca77bf60fb54b08b5b22bc85865331dd3bda7a02fa1"}, - {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b2571db88c636d862b35090ccf92bf24004393f85c8870a37f42d9f23d13e032"}, - {file = "pydantic-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8b5ac0f1c83d31b324e57a273da59197c83d1bb18171e512908fe5dc7278a1d6"}, - {file = "pydantic-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:bbbc94d0c94dd80b3340fc4f04fd4d701f4b038ebad72c39693c794fd3bc2d9d"}, - {file = "pydantic-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e0896200b6a40197405af18828da49f067c2fa1f821491bc8f5bde241ef3f7d7"}, - {file = "pydantic-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7bdfdadb5994b44bd5579cfa7c9b0e1b0e540c952d56f627eb227851cda9db77"}, - {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:574936363cd4b9eed8acdd6b80d0143162f2eb654d96cb3a8ee91d3e64bf4cf9"}, - {file = "pydantic-1.9.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c556695b699f648c58373b542534308922c46a1cda06ea47bc9ca45ef5b39ae6"}, - {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:f947352c3434e8b937e3aa8f96f47bdfe6d92779e44bb3f41e4c213ba6a32145"}, - {file = "pydantic-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5e48ef4a8b8c066c4a31409d91d7ca372a774d0212da2787c0d32f8045b1e034"}, - {file = "pydantic-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:96f240bce182ca7fe045c76bcebfa0b0534a1bf402ed05914a6f1dadff91877f"}, - {file = "pydantic-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:815ddebb2792efd4bba5488bc8fde09c29e8ca3227d27cf1c6990fc830fd292b"}, - {file = "pydantic-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6c5b77947b9e85a54848343928b597b4f74fc364b70926b3c4441ff52620640c"}, - {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c68c3bc88dbda2a6805e9a142ce84782d3930f8fdd9655430d8576315ad97ce"}, - {file = "pydantic-1.9.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a79330f8571faf71bf93667d3ee054609816f10a259a109a0738dac983b23c3"}, - {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5a64b64ddf4c99fe201ac2724daada8595ada0d102ab96d019c1555c2d6441d"}, - {file = "pydantic-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a733965f1a2b4090a5238d40d983dcd78f3ecea221c7af1497b845a9709c1721"}, - {file = "pydantic-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:2cc6a4cb8a118ffec2ca5fcb47afbacb4f16d0ab8b7350ddea5e8ef7bcc53a16"}, - {file = "pydantic-1.9.0-py3-none-any.whl", hash = "sha256:085ca1de245782e9b46cefcf99deecc67d418737a1fd3f6a4f511344b613a5b3"}, - {file = "pydantic-1.9.0.tar.gz", hash = "sha256:742645059757a56ecd886faf4ed2441b9c0cd406079c2b4bee51bcc3fbcd510a"}, -] -pyflakes = [ - {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, - {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, -] -pygit2 = [ - {file = "pygit2-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a4a9a031bb0d2c5cf964da1f6d7a193416a97664655ec43ec349d3609bbde154"}, - {file = "pygit2-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:afcfb8ba97cfedcb8f890ff1e74c4d63755234cca1ca22c2a969e91b8059ae3e"}, - {file = "pygit2-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f87498ce717302a7525dad1ee604badc85bdff7bd453473d601077ac58e7cae"}, - {file = "pygit2-1.7.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2355cf24719a35542a88075988c8e11cd155aa375a1f12d62b980986da992eb4"}, - {file = "pygit2-1.7.2-cp310-cp310-win32.whl", hash = "sha256:0d72bd05dd3cf514ea2e2df32a2d361f6f29da7d5f02cf0980ea149f49cdfb37"}, - {file = "pygit2-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:1b7ff5b656db280ca5d31ecdb17709ed7eaf4e9f419b2fa66f7ff92d8234f621"}, - {file = "pygit2-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6aa018c101056c2a8e1fb6467c10281afa088b3b7bc7c17defb404f66039669a"}, - {file = "pygit2-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a27f8cab6dbef912ccdd690b97948dbf978cffc2ef96ee01b1a8944bfb713f0b"}, - {file = "pygit2-1.7.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c538a0234baa091a02342895d31e5b7c29d85ada44a0b9b4a5fdf78b5607cd48"}, - {file = "pygit2-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:b15579b69381ba41199f5eb7fc85f153105d535c91b8da0321aaa14fec19f09c"}, - {file = "pygit2-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6c2ee00048862e193b2b88267f880632735f53db0f2c7f9ebebb21a43d22e58b"}, - {file = "pygit2-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8c24f3413522c970ae46e79b645ac0978a5be98863a6c6619e8f710bb137e1cb"}, - {file = "pygit2-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d42a7cc4b53cc369b82266c7257fe1808ec0e30c34f1796a0b0fa12a0db9ebe"}, - {file = "pygit2-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b1694ad8b4702e9e83a79a97bf3f1b44674057ae9d40bc7eb92e4b4baf79d94"}, - {file = "pygit2-1.7.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a382db82ad4ba3109e74c7b82d6c6c1e451200ee379bad8a17936027c65ea98"}, - {file = "pygit2-1.7.2-cp38-cp38-win32.whl", hash = "sha256:6c168efd7e3bdaeeccfa5ccbe2718107a1fe65cda959586f88a73228217a8783"}, - {file = "pygit2-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:041e34f7efd96c7edbea2f478704756fc189082561611c88bc95cf2d085923b5"}, - {file = "pygit2-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ef34b881da55b6702087575ea48a90a08e1077a7f64faa909d9840e16f36c63b"}, - {file = "pygit2-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0e6368a96058cf619ad574de2b4575f58d363f3f6d4de8e172e1e5d10e1fad36"}, - {file = "pygit2-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0748b413966da9b3d3ca8a0a79c63f6581a89b883d2ba64355bbfdb250f2e066"}, - {file = "pygit2-1.7.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d34954c21f109f176d8104b253fc8ce2ca17efb43cfe228d6530c200f362b83"}, - {file = "pygit2-1.7.2-cp39-cp39-win32.whl", hash = "sha256:32979cb98ffd183ed0458c519e6615deeb6a8cc1252223396eee8f526f09989f"}, - {file = "pygit2-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:56d55452dc3eca844d92503d755c8e11699b7ab3b845b81cf365f85d6385d7e0"}, - {file = "pygit2-1.7.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:409c76dea47c2c678295c42f55798da7a0a9adcc6394fe75c061864254bafeef"}, - {file = "pygit2-1.7.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be038fecd27a9a7046cd45b4a6e847955dab2d6e2352ff41ab3b55f700aa0f3d"}, - {file = "pygit2-1.7.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3e91afd629b90b528b756ca2a0fbd5bf8df2cdc08ccd5ab144fbfe69bfc587d"}, - {file = "pygit2-1.7.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:17b06a1ecc16b90fa652cf5cf9698dfb16a87501b76f7001e1d4934a38a49737"}, - {file = "pygit2-1.7.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fe7cdd56d0e5a89ed7754d1aedc6516349f16072225ccfc7b9349ab6448a052"}, - {file = "pygit2-1.7.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2a766b5a988ab373a040d1769e0e1df4618a9f8f33464746b9b2a3c92576df4"}, - {file = "pygit2-1.7.2.tar.gz", hash = "sha256:70a4536a35452c31f823b59b6fdb665aa3778a43b73ccda3a4f79fa9962ad2bb"}, -] -pyparsing = [ - {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, - {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, -] -pytest = [ - {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, - {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, -] -pytest-asyncio = [ - {file = "pytest-asyncio-0.16.0.tar.gz", hash = "sha256:7496c5977ce88c34379df64a66459fe395cd05543f0a2f837016e7144391fcfb"}, - {file = "pytest_asyncio-0.16.0-py3-none-any.whl", hash = "sha256:5f2a21273c47b331ae6aa5b36087047b4899e40f03f18397c0e65fa5cca54e9b"}, -] -pytest-cov = [ - {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, - {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, -] -pytest-forked = [ - {file = "pytest-forked-1.4.0.tar.gz", hash = "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e"}, - {file = "pytest_forked-1.4.0-py3-none-any.whl", hash = "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8"}, -] -pytest-tap = [ - {file = "pytest-tap-3.3.tar.gz", hash = "sha256:5f0919a147cf0396b2f10d64d365a0bf8062e06543e93c675c9d37f5605e983c"}, - {file = "pytest_tap-3.3-py3-none-any.whl", hash = "sha256:4fbbc0e090c2e94f6199bee4e4f68ab3c5e176b37a72a589ad84e0f72a2fce55"}, -] -pytest-xdist = [ - {file = "pytest-xdist-2.5.0.tar.gz", hash = "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf"}, - {file = "pytest_xdist-2.5.0-py3-none-any.whl", hash = "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"}, -] -python-dateutil = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, -] -python-multipart = [ - {file = "python-multipart-0.0.5.tar.gz", hash = "sha256:f7bb5f611fc600d15fa47b3974c8aa16e93724513b49b5f95c81e6624c83fa43"}, -] -redis = [ - {file = "redis-3.5.3-py2.py3-none-any.whl", hash = "sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"}, - {file = "redis-3.5.3.tar.gz", hash = "sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2"}, -] -requests = [ - {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"}, - {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, -] -rfc3986 = [ - {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, - {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, -] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] -sniffio = [ - {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, - {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, -] -sortedcontainers = [ - {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, - {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, -] -sqlalchemy = [ - {file = "SQLAlchemy-1.4.31-cp27-cp27m-macosx_10_14_x86_64.whl", hash = "sha256:c3abc34fed19fdeaead0ced8cf56dd121f08198008c033596aa6aae7cc58f59f"}, - {file = "SQLAlchemy-1.4.31-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8d0949b11681380b4a50ac3cd075e4816afe9fa4a8c8ae006c1ca26f0fa40ad8"}, - {file = "SQLAlchemy-1.4.31-cp27-cp27m-win32.whl", hash = "sha256:f3b7ec97e68b68cb1f9ddb82eda17b418f19a034fa8380a0ac04e8fe01532875"}, - {file = "SQLAlchemy-1.4.31-cp27-cp27m-win_amd64.whl", hash = "sha256:81f2dd355b57770fdf292b54f3e0a9823ec27a543f947fa2eb4ec0df44f35f0d"}, - {file = "SQLAlchemy-1.4.31-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4ad31cec8b49fd718470328ad9711f4dc703507d434fd45461096da0a7135ee0"}, - {file = "SQLAlchemy-1.4.31-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:05fa14f279d43df68964ad066f653193187909950aa0163320b728edfc400167"}, - {file = "SQLAlchemy-1.4.31-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dccff41478050e823271642837b904d5f9bda3f5cf7d371ce163f00a694118d6"}, - {file = "SQLAlchemy-1.4.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57205844f246bab9b666a32f59b046add8995c665d9ecb2b7b837b087df90639"}, - {file = "SQLAlchemy-1.4.31-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea8210090a816d48a4291a47462bac750e3bc5c2442e6d64f7b8137a7c3f9ac5"}, - {file = "SQLAlchemy-1.4.31-cp310-cp310-win32.whl", hash = "sha256:2e216c13ecc7fcdcbb86bb3225425b3ed338e43a8810c7089ddb472676124b9b"}, - {file = "SQLAlchemy-1.4.31-cp310-cp310-win_amd64.whl", hash = "sha256:e3a86b59b6227ef72ffc10d4b23f0fe994bef64d4667eab4fb8cd43de4223bec"}, - {file = "SQLAlchemy-1.4.31-cp36-cp36m-macosx_10_14_x86_64.whl", hash = "sha256:2fd4d3ca64c41dae31228b80556ab55b6489275fb204827f6560b65f95692cf3"}, - {file = "SQLAlchemy-1.4.31-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f22c040d196f841168b1456e77c30a18a3dc16b336ddbc5a24ce01ab4e95ae0"}, - {file = "SQLAlchemy-1.4.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c0c7171aa5a57e522a04a31b84798b6c926234cb559c0939840c3235cf068813"}, - {file = "SQLAlchemy-1.4.31-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d046a9aeba9bc53e88a41e58beb72b6205abb9a20f6c136161adf9128e589db5"}, - {file = "SQLAlchemy-1.4.31-cp36-cp36m-win32.whl", hash = "sha256:d86132922531f0dc5a4f424c7580a472a924dd737602638e704841c9cb24aea2"}, - {file = "SQLAlchemy-1.4.31-cp36-cp36m-win_amd64.whl", hash = "sha256:ca68c52e3cae491ace2bf39b35fef4ce26c192fd70b4cd90f040d419f70893b5"}, - {file = "SQLAlchemy-1.4.31-cp37-cp37m-macosx_10_14_x86_64.whl", hash = "sha256:cf2cd387409b12d0a8b801610d6336ee7d24043b6dd965950eaec09b73e7262f"}, - {file = "SQLAlchemy-1.4.31-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb4b15fb1f0aafa65cbdc62d3c2078bea1ceecbfccc9a1f23a2113c9ac1191fa"}, - {file = "SQLAlchemy-1.4.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c317ddd7c586af350a6aef22b891e84b16bff1a27886ed5b30f15c1ed59caeaa"}, - {file = "SQLAlchemy-1.4.31-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c7ed6c69debaf6198fadb1c16ae1253a29a7670bbf0646f92582eb465a0b999"}, - {file = "SQLAlchemy-1.4.31-cp37-cp37m-win32.whl", hash = "sha256:6a01ec49ca54ce03bc14e10de55dfc64187a2194b3b0e5ac0fdbe9b24767e79e"}, - {file = "SQLAlchemy-1.4.31-cp37-cp37m-win_amd64.whl", hash = "sha256:330eb45395874cc7787214fdd4489e2afb931bc49e0a7a8f9cd56d6e9c5b1639"}, - {file = "SQLAlchemy-1.4.31-cp38-cp38-macosx_10_14_x86_64.whl", hash = "sha256:5e9c7b3567edbc2183607f7d9f3e7e89355b8f8984eec4d2cd1e1513c8f7b43f"}, - {file = "SQLAlchemy-1.4.31-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de85c26a5a1c72e695ab0454e92f60213b4459b8d7c502e0be7a6369690eeb1a"}, - {file = "SQLAlchemy-1.4.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:975f5c0793892c634c4920057da0de3a48bbbbd0a5c86f5fcf2f2fedf41b76da"}, - {file = "SQLAlchemy-1.4.31-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5c20c8415173b119762b6110af64448adccd4d11f273fb9f718a9865b88a99c"}, - {file = "SQLAlchemy-1.4.31-cp38-cp38-win32.whl", hash = "sha256:b35dca159c1c9fa8a5f9005e42133eed82705bf8e243da371a5e5826440e65ca"}, - {file = "SQLAlchemy-1.4.31-cp38-cp38-win_amd64.whl", hash = "sha256:b7b20c88873675903d6438d8b33fba027997193e274b9367421e610d9da76c08"}, - {file = "SQLAlchemy-1.4.31-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:85e4c244e1de056d48dae466e9baf9437980c19fcde493e0db1a0a986e6d75b4"}, - {file = "SQLAlchemy-1.4.31-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e79e73d5ee24196d3057340e356e6254af4d10e1fc22d3207ea8342fc5ffb977"}, - {file = "SQLAlchemy-1.4.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:15a03261aa1e68f208e71ae3cd845b00063d242cbf8c87348a0c2c0fc6e1f2ac"}, - {file = "SQLAlchemy-1.4.31-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ddc5e5ccc0160e7ad190e5c61eb57560f38559e22586955f205e537cda26034"}, - {file = "SQLAlchemy-1.4.31-cp39-cp39-win32.whl", hash = "sha256:289465162b1fa1e7a982f8abe59d26a8331211cad4942e8031d2b7db1f75e649"}, - {file = "SQLAlchemy-1.4.31-cp39-cp39-win_amd64.whl", hash = "sha256:9e4fb2895b83993831ba2401b6404de953fdbfa9d7d4fa6a4756294a83bbc94f"}, - {file = "SQLAlchemy-1.4.31.tar.gz", hash = "sha256:582b59d1e5780a447aada22b461e50b404a9dc05768da1d87368ad8190468418"}, -] -srcinfo = [ - {file = "srcinfo-0.0.8-py3-none-any.whl", hash = "sha256:0922ee4302b927d7ddea74c47e539b226a0a7738dc89f95b66404a28d07f3f6b"}, - {file = "srcinfo-0.0.8.tar.gz", hash = "sha256:5ac610cf8b15d4b0a0374bd1f7ad301675c2938f0414addf3ef7d7e3fcaf5c65"}, -] -starlette = [ - {file = "starlette-0.17.1-py3-none-any.whl", hash = "sha256:26a18cbda5e6b651c964c12c88b36d9898481cd428ed6e063f5f29c418f73050"}, - {file = "starlette-0.17.1.tar.gz", hash = "sha256:57eab3cc975a28af62f6faec94d355a410634940f10b30d68d31cb5ec1b44ae8"}, -] -"tap.py" = [ - {file = "tap.py-3.1-py3-none-any.whl", hash = "sha256:928c852f3361707b796c93730cc5402c6378660b161114461066acf53d65bf5d"}, - {file = "tap.py-3.1.tar.gz", hash = "sha256:3c0cd45212ad5a25b35445964e2517efa000a118a1bfc3437dae828892eaf1e1"}, -] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] -tomli = [ - {file = "tomli-2.0.0-py3-none-any.whl", hash = "sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224"}, - {file = "tomli-2.0.0.tar.gz", hash = "sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1"}, -] -tomlkit = [ - {file = "tomlkit-0.9.0-py3-none-any.whl", hash = "sha256:c1b0fc73abd4f1e77c29ea4061ca0f2e11cbfb77342e17df3d3fdd496fc3f899"}, - {file = "tomlkit-0.9.0.tar.gz", hash = "sha256:5a83672c565f78f5fc8f1e44e5f2726446cc6b765113efd21d03e9331747d9ab"}, -] -typing-extensions = [ - {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, - {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, -] -urllib3 = [ - {file = "urllib3-1.26.8-py2.py3-none-any.whl", hash = "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed"}, - {file = "urllib3-1.26.8.tar.gz", hash = "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"}, -] -uvicorn = [ - {file = "uvicorn-0.15.0-py3-none-any.whl", hash = "sha256:17f898c64c71a2640514d4089da2689e5db1ce5d4086c2d53699bf99513421c1"}, - {file = "uvicorn-0.15.0.tar.gz", hash = "sha256:d9a3c0dd1ca86728d3e235182683b4cf94cd53a867c288eaeca80ee781b2caff"}, -] -webencodings = [ - {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, - {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, -] -werkzeug = [ - {file = "Werkzeug-2.0.2-py3-none-any.whl", hash = "sha256:63d3dc1cf60e7b7e35e97fa9861f7397283b75d765afcaefd993d6046899de8f"}, - {file = "Werkzeug-2.0.2.tar.gz", hash = "sha256:aa2bb6fc8dee8d6c504c0ac1e7f5f7dc5810a9903e793b6f715a9f015bdadb9a"}, -] -wsproto = [ - {file = "wsproto-1.0.0-py3-none-any.whl", hash = "sha256:d8345d1808dd599b5ffb352c25a367adb6157e664e140dbecba3f9bc007edb9f"}, - {file = "wsproto-1.0.0.tar.gz", hash = "sha256:868776f8456997ad0d9720f7322b746bbe9193751b5b290b7f924659377c8c38"}, -] -zipp = [ - {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, - {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, -] +lock-version = "2.0" +python-versions = ">=3.10,<3.14" +content-hash = "3f61efa57153b51d06808462a7cc3f2dc9a0d9132d1f35d47886a8af75243123" diff --git a/pyproject.toml b/pyproject.toml index 7a2f6ca3..d2e82441 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,11 @@ +[tool.black] +line-length = 88 + +[tool.isort] +profile = "black" +combine_as_imports = true + + # Poetry build configuration for the aurweb project. # # Dependencies: @@ -8,7 +16,7 @@ # [tool.poetry] name = "aurweb" -version = "v6.0.24" +version = "v6.2.16" license = "GPL-2.0-only" description = "Source code for the Arch User Repository's website" homepage = "https://aur.archlinux.org" @@ -21,7 +29,8 @@ authors = [ "Kevin Morris " ] maintainers = [ - "Eli Schwartz " + "Leonidas Spyropoulos ", + "Mario Oenning " ] packages = [ { include = "aurweb" } @@ -43,61 +52,67 @@ build-backend = "poetry.masonry.api" "Request Mailing List" = "https://lists.archlinux.org/listinfo/aur-requests" [tool.poetry.dependencies] -python = ">=3.9,<3.11" +python = ">=3.10,<3.14" # poetry-dynamic-versioning is used to produce tool.poetry.version # based on git tags. -poetry-dynamic-versioning = "^0.13.1" # General -aiofiles = "^0.7.0" -asgiref = "^3.4.1" -bcrypt = "^3.2.0" -bleach = "^4.1.0" -email-validator = "1.1.3" -fakeredis = "^1.6.1" -feedgen = "^0.9.0" -httpx = "^0.20.0" -itsdangerous = "^2.0.1" -lxml = "^4.6.3" -orjson = "^3.6.4" -protobuf = "^3.19.0" -pygit2 = "^1.7.0" -python-multipart = "^0.0.5" -redis = "^3.5.3" -requests = "^2.26.0" +aiofiles = "^24.0.0" +asgiref = "^3.8.1" +bcrypt = "^4.1.2" +bleach = "^6.1.0" +email-validator = "^2.1.1" +fakeredis = "^2.21.3" +feedgen = "^1.0.0" +httpx = "^0.27.0" +itsdangerous = "^2.1.2" +lxml = "^5.2.1" +orjson = "^3.10.0" +pygit2 = "^1.17.0" +python-multipart = "0.0.19" +redis = "^5.0.3" +requests = "^2.31.0" paginate = "^0.5.6" # SQL -alembic = "^1.7.4" -mysqlclient = "^2.0.3" -Authlib = "^0.15.5" -Jinja2 = "^3.0.2" -Markdown = "^3.3.6" -Werkzeug = "^2.0.2" -SQLAlchemy = "^1.4.26" +alembic = "^1.13.1" +mysqlclient = "^2.2.3" +Authlib = "^1.3.0" +Jinja2 = "^3.1.3" +Markdown = "^3.6" +Werkzeug = "^3.0.2" +SQLAlchemy = "^1.4.52" +greenlet = "3.1.1" # Explicitly add greenlet (dependency of SQLAlchemy) for python 3.13 support # ASGI -uvicorn = "^0.15.0" -gunicorn = "^20.1.0" -Hypercorn = "^0.11.2" -mysql-connector = "^2.2.9" -prometheus-fastapi-instrumentator = "^5.7.1" -pytest-xdist = "^2.4.0" -filelock = "^3.3.2" -posix-ipc = "^1.0.5" +uvicorn = "^0.30.0" +gunicorn = "^22.0.0" +Hypercorn = "^0.17.0" +pytest-xdist = "^3.5.0" +filelock = "^3.13.3" +posix-ipc = "^1.1.1" pyalpm = "^0.10.6" -fastapi = "^0.71.0" -srcinfo = "^0.0.8" +fastapi = "^0.112.0" +srcinfo = "^0.1.2" +tomlkit = "^0.13.0" + +# Tracing +prometheus-fastapi-instrumentator = "^7.0.0" +opentelemetry-api = "^1.26.0" +opentelemetry-sdk = "^1.26.0" +opentelemetry-exporter-otlp-proto-http = "^1.26.0" +opentelemetry-instrumentation-fastapi = "^0.47b0" +opentelemetry-instrumentation-redis = "^0.47b0" +opentelemetry-instrumentation-sqlalchemy = "^0.47b0" [tool.poetry.dev-dependencies] -flake8 = "^4.0.1" -isort = "^5.9.3" -coverage = "^6.0.2" -pytest = "^6.2.5" -pytest-asyncio = "^0.16.0" -pytest-cov = "^3.0.0" -pytest-tap = "^3.2" +coverage = "^7.4.4" +pytest = "^8.1.1" +pytest-asyncio = "^0.23.0" +pytest-cov = "^5.0.0" +pytest-tap = "^3.4" +watchfiles = "^1.0.4" [tool.poetry.scripts] aurweb-git-auth = "aurweb.git.auth:main" @@ -109,7 +124,8 @@ aurweb-notify = "aurweb.scripts.notify:main" aurweb-pkgmaint = "aurweb.scripts.pkgmaint:main" aurweb-popupdate = "aurweb.scripts.popupdate:main" aurweb-rendercomment = "aurweb.scripts.rendercomment:main" -aurweb-tuvotereminder = "aurweb.scripts.tuvotereminder:main" +aurweb-votereminder = "aurweb.scripts.votereminder:main" aurweb-usermaint = "aurweb.scripts.usermaint:main" aurweb-config = "aurweb.scripts.config:main" aurweb-adduser = "aurweb.scripts.adduser:main" +aurweb-git-archive = "aurweb.scripts.git_archive:main" diff --git a/pytest.ini b/pytest.ini index 9f70a2bd..62d1922a 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,13 +1,8 @@ [pytest] -# Ignore the following DeprecationWarning(s): -# - asyncio.base_events -# - DeprecationWarning speaking about internal asyncio -# using the loop= argument being deprecated starting -# with python 3.8, before python 3.10. -# - Note: This is a bug in upstream filed at -# https://bugs.python.org/issue45097 filterwarnings = - ignore::DeprecationWarning:asyncio.base_events + # This is coming from https://github.com/pytest-dev/pytest-xdist/issues/825 and it's caused from pytest-cov + # Remove once fixed: https://github.com/pytest-dev/pytest-cov/issues/557 + ignore:The --rsyncdir command line argument and rsyncdirs config variable are deprecated.:DeprecationWarning # Build in coverage and pytest-xdist multiproc testing. addopts = --cov=aurweb --cov-append --dist load --dist loadfile -n auto diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..6843bc00 --- /dev/null +++ b/renovate.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base", + "group:allNonMajor" + ], + "packageRules": [ + { + "groupName": "fastapi", + "matchPackageNames": ["fastapi"] + }, + { + "matchPackageNames": ["python"], + "enabled": false + } + ] +} diff --git a/schema/gendummydata.py b/schema/gendummydata.py index aedfda7e..a5c30170 100755 --- a/schema/gendummydata.py +++ b/schema/gendummydata.py @@ -15,30 +15,29 @@ import os import random import sys import time - -from datetime import datetime +from datetime import UTC, datetime import bcrypt LOG_LEVEL = logging.DEBUG # logging level. set to logging.INFO to reduce output SEED_FILE = "/usr/share/dict/words" -USER_ID = 5 # Users.ID of first bogus user -PKG_ID = 1 # Packages.ID of first package +USER_ID = 5 # Users.ID of first bogus user +PKG_ID = 1 # Packages.ID of first package # how many users to 'register' MAX_USERS = int(os.environ.get("MAX_USERS", 38000)) -MAX_DEVS = .1 # what percentage of MAX_USERS are Developers -MAX_TUS = .2 # what percentage of MAX_USERS are Trusted Users +MAX_DEVS = 0.1 # what percentage of MAX_USERS are Developers +MAX_PMS = 0.2 # what percentage of MAX_USERS are Package Maintainers # how many packages to load MAX_PKGS = int(os.environ.get("MAX_PKGS", 32000)) -PKG_DEPS = (1, 15) # min/max depends a package has -PKG_RELS = (1, 5) # min/max relations a package has -PKG_SRC = (1, 3) # min/max sources a package has -PKG_CMNTS = (1, 5) # min/max number of comments a package has +PKG_DEPS = (1, 15) # min/max depends a package has +PKG_RELS = (1, 5) # min/max relations a package has +PKG_SRC = (1, 3) # min/max sources a package has +PKG_CMNTS = (1, 5) # min/max number of comments a package has CATEGORIES_COUNT = 17 # the number of categories from aur-schema -VOTING = (0, .001) # percentage range for package voting -# number of open trusted user proposals +VOTING = (0, 0.001) # percentage range for package voting +# number of open package maintainer proposals OPEN_PROPOSALS = int(os.environ.get("OPEN_PROPOSALS", 15)) -# number of closed trusted user proposals +# number of closed package maintainer proposals CLOSE_PROPOSALS = int(os.environ.get("CLOSE_PROPOSALS", 50)) RANDOM_TLDS = ("edu", "com", "org", "net", "tw", "ru", "pl", "de", "es") RANDOM_URL = ("http://www.", "ftp://ftp.", "http://", "ftp://") @@ -113,10 +112,10 @@ if not len(contents) - MAX_USERS > MAX_PKGS: def normalize(unicode_data): - """ We only accept ascii for usernames. Also use this to normalize + """We only accept ascii for usernames. Also use this to normalize package names; our database utf8mb4 collations compare with Unicode - Equivalence. """ - return unicode_data.encode('ascii', 'ignore').decode('ascii') + Equivalence.""" + return unicode_data.encode("ascii", "ignore").decode("ascii") # select random usernames @@ -154,12 +153,12 @@ while len(seen_pkgs) < MAX_PKGS: # contents = None -# developer/tu IDs +# developer/PM IDs # developers = [] -trustedusers = [] +packagemaintainers = [] has_devs = 0 -has_tus = 0 +has_pms = 0 # Just let python throw the errors if any happen # @@ -171,7 +170,7 @@ out.write("BEGIN;\n") log.debug("Creating SQL statements for users.") for u in user_keys: account_type = 1 # default to normal user - if not has_devs or not has_tus: + if not has_devs or not has_pms: account_type = random.randrange(1, 4) if account_type == 3 and not has_devs: # this will be a dev account @@ -179,12 +178,12 @@ for u in user_keys: developers.append(seen_users[u]) if len(developers) >= MAX_DEVS * MAX_USERS: has_devs = 1 - elif account_type == 2 and not has_tus: - # this will be a trusted user account + elif account_type == 2 and not has_pms: + # this will be a package maintainer account # - trustedusers.append(seen_users[u]) - if len(trustedusers) >= MAX_TUS * MAX_USERS: - has_tus = 1 + packagemaintainers.append(seen_users[u]) + if len(packagemaintainers) >= MAX_PMS * MAX_USERS: + has_pms = 1 else: # a normal user account # @@ -196,16 +195,20 @@ for u in user_keys: # "{salt}{username}" to_hash = f"{salt}{u}" - h = hashlib.new('md5') + h = hashlib.new("md5") h.update(to_hash.encode()) - s = ("INSERT INTO Users (ID, AccountTypeID, Username, Email, Passwd, Salt)" - " VALUES (%d, %d, '%s', '%s@example.com', '%s', '%s');\n") + s = ( + "INSERT INTO Users (ID, AccountTypeID, Username, Email, Passwd, Salt)" + " VALUES (%d, %d, '%s', '%s@example.com', '%s', '%s');\n" + ) s = s % (seen_users[u], account_type, u, u, h.hexdigest(), salt) out.write(s) log.debug("Number of developers: %d" % len(developers)) -log.debug("Number of trusted users: %d" % len(trustedusers)) -log.debug("Number of users: %d" % (MAX_USERS - len(developers) - len(trustedusers))) +log.debug("Number of package maintainers: %d" % len(packagemaintainers)) +log.debug( + "Number of users: %d" % (MAX_USERS - len(developers) - len(packagemaintainers)) +) log.debug("Number of packages: %d" % MAX_PKGS) log.debug("Gathering text from fortune file...") @@ -223,20 +226,25 @@ for p in list(seen_pkgs.keys()): muid = developers[random.randrange(0, len(developers))] puid = developers[random.randrange(0, len(developers))] else: - muid = trustedusers[random.randrange(0, len(trustedusers))] - puid = trustedusers[random.randrange(0, len(trustedusers))] + muid = packagemaintainers[random.randrange(0, len(packagemaintainers))] + puid = packagemaintainers[random.randrange(0, len(packagemaintainers))] if count % 20 == 0: # every so often, there are orphans... muid = "NULL" uuid = genUID() # the submitter/user - s = ("INSERT INTO PackageBases (ID, Name, FlaggerComment, SubmittedTS, ModifiedTS, " - "SubmitterUID, MaintainerUID, PackagerUID) VALUES (%d, '%s', '', %d, %d, %d, %s, %s);\n") + s = ( + "INSERT INTO PackageBases (ID, Name, FlaggerComment, SubmittedTS, ModifiedTS, " + "SubmitterUID, MaintainerUID, PackagerUID) VALUES " + "(%d, '%s', '', %d, %d, %d, %s, %s);\n" + ) s = s % (seen_pkgs[p], p, NOW, NOW, uuid, muid, puid) out.write(s) - s = ("INSERT INTO Packages (ID, PackageBaseID, Name, Version) VALUES " - "(%d, %d, '%s', '%s');\n") + s = ( + "INSERT INTO Packages (ID, PackageBaseID, Name, Version) VALUES " + "(%d, %d, '%s', '%s');\n" + ) s = s % (seen_pkgs[p], seen_pkgs[p], p, genVersion()) out.write(s) @@ -247,25 +255,30 @@ for p in list(seen_pkgs.keys()): num_comments = random.randrange(PKG_CMNTS[0], PKG_CMNTS[1]) for i in range(0, num_comments): now = NOW + random.randrange(400, 86400 * 3) - s = ("INSERT INTO PackageComments (PackageBaseID, UsersID," - " Comments, RenderedComment, CommentTS) VALUES (%d, %d, '%s', '', %d);\n") + s = ( + "INSERT INTO PackageComments (PackageBaseID, UsersID," + " Comments, RenderedComment, CommentTS) VALUES (%d, %d, '%s', '', %d);\n" + ) s = s % (seen_pkgs[p], genUID(), genFortune(), now) out.write(s) # Cast votes -utcnow = int(datetime.utcnow().timestamp()) +utcnow = int(datetime.now(UTC).timestamp()) track_votes = {} log.debug("Casting votes for packages.") for u in user_keys: - num_votes = random.randrange(int(len(seen_pkgs) * VOTING[0]), - int(len(seen_pkgs) * VOTING[1])) + num_votes = random.randrange( + int(len(seen_pkgs) * VOTING[0]), int(len(seen_pkgs) * VOTING[1]) + ) pkgvote = {} for v in range(num_votes): pkg = random.randrange(1, len(seen_pkgs) + 1) if pkg not in pkgvote: - s = ("INSERT INTO PackageVotes (UsersID, PackageBaseID, VoteTS)" - " VALUES (%d, %d, %d);\n") + s = ( + "INSERT INTO PackageVotes (UsersID, PackageBaseID, VoteTS)" + " VALUES (%d, %d, %d);\n" + ) s = s % (seen_users[u], pkg, utcnow) pkgvote[pkg] = 1 if pkg not in track_votes: @@ -293,7 +306,10 @@ for p in seen_pkgs_keys: deptype = random.randrange(1, 5) if deptype == 4: dep += ": for " + random.choice(seen_pkgs_keys) - s = "INSERT INTO PackageDepends(PackageID, DepTypeID, DepName) VALUES (%d, %d, '%s');\n" + s = ( + "INSERT INTO PackageDepends(PackageID, DepTypeID, DepName) " + "VALUES (%d, %d, '%s');\n" + ) s = s % (seen_pkgs[p], deptype, dep) out.write(s) @@ -301,7 +317,10 @@ for p in seen_pkgs_keys: for i in range(0, num_deps): rel = random.choice(seen_pkgs_keys) reltype = random.randrange(1, 4) - s = "INSERT INTO PackageRelations(PackageID, RelTypeID, RelName) VALUES (%d, %d, '%s');\n" + s = ( + "INSERT INTO PackageRelations(PackageID, RelTypeID, RelName) " + "VALUES (%d, %d, '%s');\n" + ) s = s % (seen_pkgs[p], reltype, rel) out.write(s) @@ -310,16 +329,19 @@ for p in seen_pkgs_keys: src_file = user_keys[random.randrange(0, len(user_keys))] src = "%s%s.%s/%s/%s-%s.tar.gz" % ( RANDOM_URL[random.randrange(0, len(RANDOM_URL))], - p, RANDOM_TLDS[random.randrange(0, len(RANDOM_TLDS))], + p, + RANDOM_TLDS[random.randrange(0, len(RANDOM_TLDS))], RANDOM_LOCS[random.randrange(0, len(RANDOM_LOCS))], - src_file, genVersion()) + src_file, + genVersion(), + ) s = "INSERT INTO PackageSources(PackageID, Source) VALUES (%d, '%s');\n" s = s % (seen_pkgs[p], src) out.write(s) -# Create trusted user proposals +# Create package maintainer proposals # -log.debug("Creating SQL statements for trusted user proposals.") +log.debug("Creating SQL statements for package maintainer proposals.") count = 0 for t in range(0, OPEN_PROPOSALS + CLOSE_PROPOSALS): now = int(time.time()) @@ -333,9 +355,11 @@ for t in range(0, OPEN_PROPOSALS + CLOSE_PROPOSALS): user = "" else: user = user_keys[random.randrange(0, len(user_keys))] - suid = trustedusers[random.randrange(0, len(trustedusers))] - s = ("INSERT INTO TU_VoteInfo (Agenda, User, Submitted, End," - " Quorum, SubmitterID) VALUES ('%s', '%s', %d, %d, 0.0, %d);\n") + suid = packagemaintainers[random.randrange(0, len(packagemaintainers))] + s = ( + "INSERT INTO VoteInfo (Agenda, User, Submitted, End," + " Quorum, SubmitterID) VALUES ('%s', '%s', %d, %d, 0.0, %d);\n" + ) s = s % (genFortune(), user, start, end, suid) out.write(s) count += 1 diff --git a/setup.cfg b/setup.cfg index 08be9186..41978dae 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,9 +1,5 @@ -[pycodestyle] -max-line-length = 127 -ignore = E741, W503 - [flake8] -max-line-length = 127 +max-line-length = 88 max-complexity = 10 # Ignore some unavoidable flake8 warnings; we know this is against @@ -17,9 +13,4 @@ max-complexity = 10 # With {W503,W504}, PEP8 does not want us to break lines before # or after a binary operator. We have many scripts that already # do this, so we're ignoring it here. -ignore = E741, W503, W504 - -[isort] -line_length = 127 -lines_between_types = 1 - +ignore = E203, E741, W503, W504 diff --git a/web/html/css/archnavbar/archlogo.png b/static/css/archnavbar/archlogo.png similarity index 100% rename from web/html/css/archnavbar/archlogo.png rename to static/css/archnavbar/archlogo.png diff --git a/web/html/css/archnavbar/archnavbar.css b/static/css/archnavbar/archnavbar.css similarity index 100% rename from web/html/css/archnavbar/archnavbar.css rename to static/css/archnavbar/archnavbar.css diff --git a/web/html/css/archnavbar/aurlogo.png b/static/css/archnavbar/aurlogo.png similarity index 100% rename from web/html/css/archnavbar/aurlogo.png rename to static/css/archnavbar/aurlogo.png diff --git a/web/html/css/archweb.css b/static/css/archweb.css similarity index 96% rename from web/html/css/archweb.css rename to static/css/archweb.css index 61dd05c7..2d0728c4 100644 --- a/web/html/css/archweb.css +++ b/static/css/archweb.css @@ -23,7 +23,7 @@ #archnavbarlogo { float: left !important; margin: 0 !important; padding: 0 !important; height: 40px !important; width: 190px !important; background: url('archnavbar/archlogo.png') no-repeat !important; } @media (-webkit-min-device-pixel-ratio: 1.2), (min--moz-device-pixel-ratio: 1.2), (-o-min-device-pixel-ratio: 2/1) { [dir="rtl"] #archnavbarlogo { float: right !important;} - #archnavbarlogo { float: left !important; margin: 0 !important; padding: 0 !important; height: 40px !important; width: 190px !important; background: url(archnavbar/archlogo.svg) no-repeat !important;background-size:100% !important; + #archnavbarlogo { float: left !important; margin: 0 !important; padding: 0 !important; height: 40px !important; width: 190px !important; background: url(archnavbar/archlogo.svg) no-repeat !important;background-size:100% !important; } } @@ -345,11 +345,14 @@ dl { border-top: 1px dotted #bbb; } - [dir="rtl"] dl dt {float: right; padding-left: 15px;} + [dir="rtl"] dl dt { + float: right; + padding-left: 15px; + } dl dt { color: #333; - float:left; - padding-right:15px; + float: left; + padding-right: 15px; } /* forms and input styling */ @@ -502,7 +505,9 @@ table thead th.sorter-false { #news .more { font-weight: normal; } - + [dir="rtl"] #news .rss-icon { + float: left; + } #news .rss-icon { float: right; margin-top: 1em; @@ -514,7 +519,10 @@ table thead th.sorter-false { margin-top: 1.5em; border-bottom: 1px dotted #bbb; } - + [dir="rtl"] #news .timestamp { + float: left; + margin: -1.8em 0 0 0.5em; + } #news .timestamp { float: right; font-size: 0.85em; @@ -559,7 +567,9 @@ h3 span.arrow { padding: 0.1em 0; } - [dir="rtl"] #pkgsearch input {float: left;} + [dir="rtl"] #pkgsearch input { + float: left; + } #pkgsearch input { width: 10em; float: right; @@ -569,7 +579,11 @@ h3 span.arrow { border: 1px solid #09c; } - [dir="rtl"] .pkgsearch-typeahead {right:0;float:right;text-align: right;} + [dir="rtl"] .pkgsearch-typeahead { + right: 0; + float: right; + text-align: right; + } .pkgsearch-typeahead { position: absolute; @@ -606,7 +620,8 @@ h3 span.arrow { font-weight: normal; } [dir="rtl"] #pkg-updates .rss-icon { - float: left;} + float: left; + } #pkg-updates .rss-icon { float: right; margin: -2em 0 0 0; @@ -741,6 +756,9 @@ div.news-article .article-info { width: 100%; } /* max 4 columns, but possibly fewer if screen size doesn't allow for more */ + [dir="rtl"] #donor-list li { + float: right; + } #donor-list li { float: left; width: 25%; @@ -845,7 +863,7 @@ table.results { /* pkgdetails: details links that float on the right */ [dir="rtl"] #pkgdetails #detailslinks { - float: right; + float: left; } #pkgdetails #detailslinks { float: right; @@ -952,6 +970,7 @@ table.results { [dir="rtl"] #pkgdetails #pkgdeps { float: right; + width: 48%; margin-left: 2%; } @@ -973,6 +992,7 @@ table.results { [dir="rtl"] #pkgdetails #pkgreqs { float: right; + width: 48%; } #pkgdetails #pkgreqs { @@ -981,7 +1001,7 @@ table.results { } #pkgdetails #pkgfiles { - clear: left; + clear: both; padding-top: 1em; } @@ -1024,7 +1044,7 @@ table td.country { stroke-width: 1.5px; } -/* dev/TU biographies */ +/* dev/PM biographies */ #arch-bio-toc { width: 75%; margin: 0 auto; @@ -1114,7 +1134,9 @@ table.dash-stats .key { } /* dev dashboard: admin actions (add news items, todo list, etc) */ -[dir="rtl"] ul.admin-actions {float: left;} +[dir="rtl"] ul.admin-actions { + float: left; +} ul.admin-actions { float: right; list-style: none; @@ -1230,4 +1252,4 @@ ul.signoff-list { /* itemprops */ .itemprop { display: none; -} \ No newline at end of file +} diff --git a/web/html/css/aurweb.css b/static/css/aurweb.css similarity index 94% rename from web/html/css/aurweb.css rename to static/css/aurweb.css index 22b5ac65..64a65742 100644 --- a/web/html/css/aurweb.css +++ b/static/css/aurweb.css @@ -125,13 +125,11 @@ } .rss-icon, .delete-comment, .undelete-comment, .edit-comment, .pin-comment { - -webkit-filter: grayscale(100%); filter: grayscale(100%); opacity: 0.6; } .rss-icon:hover, .delete-comment:hover, .undelete-comment:hover, .edit-comment:hover, .pin-comment:hover { - -webkit-filter: none; filter: none; opacity: 1; } @@ -195,6 +193,11 @@ label.confirmation { align-self: flex-end; } +.comments-footer { + display: flex; + justify-content: flex-end; +} + .comment-header { clear: both; font-size: 1em; @@ -277,8 +280,13 @@ div.box form.link button { pre.traceback { /* https://css-tricks.com/snippets/css/make-pre-text-wrap/ */ white-space: pre-wrap; - white-space: -moz-pre-wrap; - white-space: -pre-wrap; - white-space: -o-pre-wrap; word-wrap: break-all; } + +/* By default, tables use 100% width, which we do not always want. */ +table.no-width { + width: auto; +} +table.no-width > tbody > tr > td { + padding-right: 2px; +} diff --git a/web/html/css/cgit.css b/static/css/cgit.css similarity index 100% rename from web/html/css/cgit.css rename to static/css/cgit.css diff --git a/web/template/cgit/footer.html b/static/html/cgit/footer.html similarity index 70% rename from web/template/cgit/footer.html rename to static/html/cgit/footer.html index 14c358f1..b3e79568 100644 --- a/web/template/cgit/footer.html +++ b/static/html/cgit/footer.html @@ -1,6 +1,6 @@ diff --git a/web/template/cgit/header.html b/static/html/cgit/header.html similarity index 90% rename from web/template/cgit/header.html rename to static/html/cgit/header.html index 2d418702..c5ef0f6b 100644 --- a/web/template/cgit/header.html +++ b/static/html/cgit/header.html @@ -6,7 +6,7 @@
  • Packages
  • Forums
  • Wiki
  • -
  • Bugs
  • +
  • GitLab
  • Security
  • AUR
  • Download
  • diff --git a/web/html/images/ICON-LICENSE b/static/images/ICON-LICENSE similarity index 100% rename from web/html/images/ICON-LICENSE rename to static/images/ICON-LICENSE diff --git a/web/html/images/action-undo.min.svg b/static/images/action-undo.min.svg similarity index 100% rename from web/html/images/action-undo.min.svg rename to static/images/action-undo.min.svg diff --git a/web/html/images/action-undo.svg b/static/images/action-undo.svg similarity index 100% rename from web/html/images/action-undo.svg rename to static/images/action-undo.svg diff --git a/web/html/images/ajax-loader.gif b/static/images/ajax-loader.gif similarity index 100% rename from web/html/images/ajax-loader.gif rename to static/images/ajax-loader.gif diff --git a/web/html/images/favicon.ico b/static/images/favicon.ico similarity index 100% rename from web/html/images/favicon.ico rename to static/images/favicon.ico diff --git a/web/html/images/pencil.min.svg b/static/images/pencil.min.svg similarity index 100% rename from web/html/images/pencil.min.svg rename to static/images/pencil.min.svg diff --git a/web/html/images/pencil.svg b/static/images/pencil.svg similarity index 100% rename from web/html/images/pencil.svg rename to static/images/pencil.svg diff --git a/web/html/images/pin.min.svg b/static/images/pin.min.svg similarity index 100% rename from web/html/images/pin.min.svg rename to static/images/pin.min.svg diff --git a/web/html/images/pin.svg b/static/images/pin.svg similarity index 100% rename from web/html/images/pin.svg rename to static/images/pin.svg diff --git a/web/html/images/rss.svg b/static/images/rss.svg similarity index 100% rename from web/html/images/rss.svg rename to static/images/rss.svg diff --git a/web/html/images/unpin.min.svg b/static/images/unpin.min.svg similarity index 100% rename from web/html/images/unpin.min.svg rename to static/images/unpin.min.svg diff --git a/web/html/images/unpin.svg b/static/images/unpin.svg similarity index 100% rename from web/html/images/unpin.svg rename to static/images/unpin.svg diff --git a/web/html/images/x.min.svg b/static/images/x.min.svg similarity index 100% rename from web/html/images/x.min.svg rename to static/images/x.min.svg diff --git a/web/html/images/x.svg b/static/images/x.svg similarity index 100% rename from web/html/images/x.svg rename to static/images/x.svg diff --git a/web/html/js/comment-edit.js b/static/js/comment-edit.js similarity index 100% rename from web/html/js/comment-edit.js rename to static/js/comment-edit.js diff --git a/web/html/js/copy.js b/static/js/copy.js similarity index 100% rename from web/html/js/copy.js rename to static/js/copy.js diff --git a/web/html/js/typeahead-home.js b/static/js/typeahead-home.js similarity index 100% rename from web/html/js/typeahead-home.js rename to static/js/typeahead-home.js diff --git a/web/html/js/typeahead-pkgbase-merge.js b/static/js/typeahead-pkgbase-merge.js similarity index 100% rename from web/html/js/typeahead-pkgbase-merge.js rename to static/js/typeahead-pkgbase-merge.js diff --git a/web/html/js/typeahead-pkgbase-request.js b/static/js/typeahead-pkgbase-request.js similarity index 100% rename from web/html/js/typeahead-pkgbase-request.js rename to static/js/typeahead-pkgbase-request.js diff --git a/static/js/typeahead-requests.js b/static/js/typeahead-requests.js new file mode 100644 index 00000000..9f636eab --- /dev/null +++ b/static/js/typeahead-requests.js @@ -0,0 +1,6 @@ +document.addEventListener('DOMContentLoaded', function() { + const input = document.getElementById('id_filter_pkg_name'); + const form = document.getElementById('todolist_filter'); + const type = 'suggest-pkgbase'; + typeahead.init(type, input, form); +}); diff --git a/web/html/js/typeahead.js b/static/js/typeahead.js similarity index 100% rename from web/html/js/typeahead.js rename to static/js/typeahead.js diff --git a/templates/account/delete.html b/templates/account/delete.html new file mode 100644 index 00000000..625d3c2d --- /dev/null +++ b/templates/account/delete.html @@ -0,0 +1,43 @@ +{% extends "partials/layout.html" %} + +{% block pageContent %} +
    +

    {{ "Accounts" | tr }}

    + + {% include "partials/error.html" %} + +

    + {{ + "You can use this form to permanently delete the AUR account %s%s%s." + | tr | format("", name, "") | safe + }} +

    + +

    + {{ + "%sWARNING%s: This action cannot be undone." + | tr | format("", "") | safe + }} +

    + + +
    +
    +

    + + +

    +

    + +

    +

    + +

    +
    +
    + +
    +{% endblock %} diff --git a/templates/account/search.html b/templates/account/search.html index d28d4169..325ef0ba 100644 --- a/templates/account/search.html +++ b/templates/account/search.html @@ -20,9 +20,9 @@

    diff --git a/templates/account/show.html b/templates/account/show.html index a9bb3c30..a57efb77 100644 --- a/templates/account/show.html +++ b/templates/account/show.html @@ -25,7 +25,11 @@ {% trans %}Email Address{% endtrans %}: + {% if not user.HideEmail %} {{ user.Email }} + {% else %} + <{% trans %}hidden{% endtrans %}> + {% endif %} diff --git a/templates/addvote.html b/templates/addvote.html index 4d2b0292..cc12f42b 100644 --- a/templates/addvote.html +++ b/templates/addvote.html @@ -19,26 +19,26 @@

    - +

    + +

    + + + +

    + {% if form_type == "UpdateAccount" %} @@ -233,7 +251,7 @@ -

    @@ -242,7 +260,7 @@ {% trans %}Re-type password{% endtrans %}: -

    @@ -264,6 +282,13 @@

    +

    + + {{ + "Specify multiple SSH Keys separated by new line, empty lines are ignored." | tr + }} + +

    @@ -313,7 +338,7 @@ -

    {% else %} diff --git a/templates/partials/archdev-navbar.html b/templates/partials/archdev-navbar.html index 98bb1841..ea67b1ca 100644 --- a/templates/partials/archdev-navbar.html +++ b/templates/partials/archdev-navbar.html @@ -36,10 +36,10 @@ - {# Only CRED_TU_LIST_VOTES privileged users see Trusted User #} - {% if request.user.has_credential(creds.TU_LIST_VOTES) %} + {# Only CRED_PM_LIST_VOTES privileged users see Package Maintainer #} + {% if request.user.has_credential(creds.PM_LIST_VOTES) %}
  • - {% trans %}Trusted User{% endtrans %} + {% trans %}Package Maintainer{% endtrans %}
  • {% endif %} diff --git a/templates/partials/navbar.html b/templates/partials/navbar.html index 199b2067..00ceab6b 100644 --- a/templates/partials/navbar.html +++ b/templates/partials/navbar.html @@ -10,7 +10,7 @@
  • {% trans %}Packages{% endtrans %}
  • {% trans %}Forums{% endtrans %}
  • {% trans %}Wiki{% endtrans %}
  • -
  • {% trans %}Bugs{% endtrans %}
  • +
  • {% trans %}GitLab{% endtrans %}
  • {% trans %}Security{% endtrans %}
  • AUR
  • {% trans %}Download{% endtrans %}
  • diff --git a/templates/partials/tu/last_votes.html b/templates/partials/package-maintainer/last_votes.html similarity index 92% rename from templates/partials/tu/last_votes.html rename to templates/partials/package-maintainer/last_votes.html index 30d620d4..4f30d9ff 100644 --- a/templates/partials/tu/last_votes.html +++ b/templates/partials/package-maintainer/last_votes.html @@ -24,7 +24,7 @@ - + {{ vote.LastVote }} diff --git a/templates/partials/tu/proposal/details.html b/templates/partials/package-maintainer/proposal/details.html similarity index 92% rename from templates/partials/tu/proposal/details.html rename to templates/partials/package-maintainer/proposal/details.html index f7a55148..24b21262 100644 --- a/templates/partials/tu/proposal/details.html +++ b/templates/partials/package-maintainer/proposal/details.html @@ -21,6 +21,11 @@ +
    + {{ "Active" | tr }} {{ "Package Maintainers" | tr }} {{ "assigned" | tr }}: + {{ voteinfo.ActiveUsers }} +
    + {% set submitter = voteinfo.Submitter.Username %} {% set submitter_uri = "/account/%s" | format(submitter) %} {% set submitter = '%s' | format(submitter_uri, submitter) %} @@ -42,7 +47,7 @@ {% if not voteinfo.is_running() %}
    {{ "Result" | tr }}: - {% if not voteinfo.ActiveTUs %} + {% if not voteinfo.ActiveUsers %} {{ "unknown" | tr }} {% elif accepted %} @@ -97,7 +102,7 @@ {% endif %} - {% if voteinfo.ActiveTUs %} + {% if voteinfo.ActiveUsers %} {{ (participation * 100) | number_format(2) }}% {% else %} {{ "unknown" | tr }} diff --git a/templates/partials/tu/proposal/form.html b/templates/partials/package-maintainer/proposal/form.html similarity index 84% rename from templates/partials/tu/proposal/form.html rename to templates/partials/package-maintainer/proposal/form.html index d783a622..e172b29d 100644 --- a/templates/partials/tu/proposal/form.html +++ b/templates/partials/package-maintainer/proposal/form.html @@ -1,4 +1,4 @@ -
    +
    - {% if comment and not request.user.notified(pkgbase) %} + {% if comment %} + + {% endif %} + {% if not request.user.notified(pkgbase) %} diff --git a/templates/partials/packages/comments.html b/templates/partials/packages/comments.html index 6e6b9a47..55421bfa 100644 --- a/templates/partials/packages/comments.html +++ b/templates/partials/packages/comments.html @@ -33,9 +33,24 @@ {{ "Latest Comments" | tr }} + {% set page = ((O / PP) | int) %} + {% set pages = ((comments_total / PP) | ceil) %} + + {% if pages > 1 %} +

    + {{ page | pager_nav(comments_total, prefix) | safe }} +

    + {% endif %}
    {% for comment in comments.all() %} {% include "partials/packages/comment.html" %} {% endfor %} + {% endif %} diff --git a/templates/partials/packages/details.html b/templates/partials/packages/details.html index e0eda54c..5f242414 100644 --- a/templates/partials/packages/details.html +++ b/templates/partials/packages/details.html @@ -1,4 +1,3 @@ -{% set pkg = pkgbase.packages.first() %} @@ -18,25 +17,25 @@ - + - + {% endif %} - {% if pkgbase.keywords.count() or request.user.has_credential(creds.PKGBASE_SET_KEYWORDS, approved=[pkgbase.Maintainer]) %} + {% if pkgbase.keywords.count() or request.user.has_credential(creds.PKGBASE_SET_KEYWORDS, approved=[pkgbase.Maintainer] + comaintainers) %} - {% if request.user.has_credential(creds.PKGBASE_SET_KEYWORDS, approved=[pkgbase.Maintainer]) %} + {% if request.user.has_credential(creds.PKGBASE_SET_KEYWORDS, approved=[pkgbase.Maintainer] + comaintainers) %} {% endif %} + {% if show_package_details and groups and groups.count() %} + + + + + {% endif %} {% if show_package_details and conflicts and conflicts.count() %} @@ -108,7 +113,7 @@ @@ -147,7 +155,7 @@ - + diff --git a/templates/partials/packages/package_metadata.html b/templates/partials/packages/package_metadata.html index 6f58c2be..2dd39c20 100644 --- a/templates/partials/packages/package_metadata.html +++ b/templates/partials/packages/package_metadata.html @@ -1,5 +1,5 @@
    -

    {{ "Dependencies" | tr }} ({{ dependencies | length }})

    +

    {{ "Dependencies" | tr }} ({{ depends_count }})

    -

    {{ "Required by" | tr }} ({{ required_by | length }})

    +

    {{ "Required by" | tr }} ({{ reqs_count }})

    diff --git a/templates/partials/packages/search_actions.html b/templates/partials/packages/search_actions.html index f28e27a9..c84ad93a 100644 --- a/templates/partials/packages/search_actions.html +++ b/templates/partials/packages/search_actions.html @@ -4,7 +4,7 @@ - {% if request.user.is_trusted_user() or request.user.is_developer() %} + {% if request.user.is_package_maintainer() or request.user.is_developer() %} {% endif %} diff --git a/templates/partials/packages/search_results.html b/templates/partials/packages/search_results.html index 84c39079..a365a1d3 100644 --- a/templates/partials/packages/search_results.html +++ b/templates/partials/packages/search_results.html @@ -68,6 +68,16 @@ {{ "Maintainer" | tr }} + @@ -115,6 +125,11 @@ {{ "orphan" | tr }} {% endif %} + {% if flagged %} + + {% else %} + + {% endif %} {% endfor %} diff --git a/templates/partials/packages/statistics.html b/templates/partials/packages/statistics.html index 8a6546b9..7ce5fba1 100644 --- a/templates/partials/packages/statistics.html +++ b/templates/partials/packages/statistics.html @@ -42,9 +42,9 @@ - +
    {{ "Git Clone URL" | tr }}:
    {{ "Description" | tr }}:{{ pkg.Description }}{{ package.Description }}
    {{ "Upstream URL" | tr }}: - {% if pkg.URL %} - {{ pkg.URL }} + {% if package.URL %} + {{ package.URL }} {% else %} {{ "None" | tr }} {% endif %}
    {{ "Keywords" | tr }}:
    {% for keyword in pkgbase.keywords.all() %} {{ keyword.Keyword }} @@ -69,6 +68,12 @@
    {{ licenses.all() | join(', ', attribute='License.Name') }}
    {{ "Groups" | tr }}:{{ groups.all() | join(', ', attribute='Group.Name') }}
    {{ "Conflicts" | tr }}:
    {{ "Maintainer" | tr }}: - {% if pkgbase.Maintainer %} + {% if request.user.is_authenticated() and pkgbase.Maintainer %} {{ pkgbase.Maintainer.Username }} @@ -118,6 +123,9 @@ {% endif %} {% else %} {{ pkgbase.Maintainer.Username | default("None" | tr) }} + {% if comaintainers %} + ({{ comaintainers|join(', ') }}) + {% endif %} {% endif %}
    {{ "Popularity" | tr }}:{{ pkgbase.Popularity | number_format(6 if pkgbase.Popularity <= 0.2 else 2) }}{{ popularity | number_format(6 if popularity <= 0.2 else 2) }}
    {{ "First Submitted" | tr }}: + {% if SB == "l" %} + {% set order = reverse_order %} + {% else %} + {% set order = SO %} + {% endif %} + + {{ "Last Updated" | tr }} + +
    {{ datetime_display(pkg.ModifiedTS) }}{{ datetime_display(pkg.ModifiedTS) }}
    - {{ "Trusted Users" | tr }} + {{ "Package Maintainers" | tr }} {{ trusted_user_count }}{{ package_maintainer_count }}
    diff --git a/templates/partials/support.html b/templates/partials/support.html new file mode 100644 index 00000000..b175a040 --- /dev/null +++ b/templates/partials/support.html @@ -0,0 +1,65 @@ +

    {% trans %}Support{% endtrans %}

    +

    {% trans %}Package Requests{% endtrans %}

    +
    +

    + {{ "There are three types of requests that can be filed in the %sPackage Actions%s box on the package details page:" + | tr + | format("", "") + | safe + }} +

    +
      +
    • {% trans %}Orphan Request{% endtrans %}: {% trans %}Request a package to be disowned, e.g. when the maintainer is inactive and the package has been flagged out-of-date for a long time.{% endtrans %}
    • +
    • {% trans %}Deletion Request{% endtrans %}: {%trans %}Request a package to be removed from the Arch User Repository. Please do not use this if a package is broken and can be fixed easily. Instead, contact the maintainer and file orphan request if necessary.{% endtrans %}
    • +
    • {% trans %}Merge Request{% endtrans %}: {% trans %}Request a package to be merged into another one. Can be used when a package needs to be renamed or replaced by a split package.{% endtrans %}
    • +
    +

    +{{ "If you want to discuss a request, you can use the %saur-requests%s mailing list. However, please do not use that list to file requests." + | tr + | format('', "") + | safe + }} +

    +
    +

    {% trans %}Submitting Packages{% endtrans %}

    +
    +

    + {{ "Git over SSH is now used to submit packages to the AUR. See the %sSubmitting packages%s section of the Arch User Repository ArchWiki page for more details." + | tr + | format('', "") + | safe + }} +

    +{% if ssh_fingerprints %} +

    + {% trans %}The following SSH fingerprints are used for the AUR:{% endtrans %} +

    +

      + {% for keytype in ssh_fingerprints %} +
    • {{ keytype }}: {{ ssh_fingerprints[keytype] }} + {% endfor %} +
    +{% endif %} +
    +

    {% trans %}Discussion{% endtrans %}

    +
    +

    + {{ "General discussion regarding the Arch User Repository (AUR) and Package Maintainer structure takes place on %saur-general%s. For discussion relating to the development of the AUR web interface, use the %saur-dev%s mailing list." + | tr + | format('', "", + '', "") + | safe + }} +

    +

    +

    {% trans %}Bug Reporting{% endtrans %}

    +
    +

    + {{ "If you find a bug in the AUR web interface, please fill out a bug report on our %sbug tracker%s. Use the tracker to report bugs in the AUR web interface %sonly%s. To report packaging bugs contact the maintainer or leave a comment on the appropriate package page." + | tr + | format('', "", + "", "") + | safe + }} +

    +
    diff --git a/templates/passreset.html b/templates/passreset.html index d2c3c2ee..08493fe9 100644 --- a/templates/passreset.html +++ b/templates/passreset.html @@ -26,14 +26,14 @@ {% trans %}Enter your new password:{% endtrans %} - {% trans %}Confirm your new password:{% endtrans %} - @@ -47,7 +47,7 @@ {% else %} - {% set url = "https://mailman.archlinux.org/mailman/listinfo/aur-general" %} + {% set url = "https://lists.archlinux.org/mailman3/lists/aur-general.lists.archlinux.org/" %} {{ "If you have forgotten the user name and the primary e-mail " "address you used to register, please send a message to the " "%saur-general%s mailing list." diff --git a/templates/pkgbase/comments/edit.html b/templates/pkgbase/comments/edit.html index f938287e..0d4ec28a 100644 --- a/templates/pkgbase/comments/edit.html +++ b/templates/pkgbase/comments/edit.html @@ -24,13 +24,17 @@ "" ) | safe }} +
    + {{ "Maximum number of characters" | tr }}: {{ max_chars_comment }}.

    + rows="10" + maxlength="{{ max_chars_comment }}" + >{{ comment.Comments }}

    diff --git a/templates/pkgbase/delete.html b/templates/pkgbase/delete.html index 5bd74b72..defcf58f 100644 --- a/templates/pkgbase/delete.html +++ b/templates/pkgbase/delete.html @@ -50,7 +50,7 @@

    diff --git a/templates/pkgbase/disown.html b/templates/pkgbase/disown.html index 3cc7988d..c01398c8 100644 --- a/templates/pkgbase/disown.html +++ b/templates/pkgbase/disown.html @@ -27,14 +27,16 @@ {% endfor %} -

    - {{ - "This action will close any pending package requests " - "related to it. If %sComments%s are omitted, a closure " - "comment will be autogenerated." - | tr | format("", "") | safe - }} -

    + {% if not is_maint and not is_comaint %} +

    + {{ + "This action will close any pending package requests " + "related to it. If %sComments%s are omitted, a closure " + "comment will be autogenerated." + | tr | format("", "") | safe + }} +

    + {% endif %}

    {{ @@ -47,14 +49,19 @@

    -

    - - -

    + {% if not is_maint and not is_comaint %} +

    + + +

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

    diff --git a/templates/pkgbase/merge.html b/templates/pkgbase/merge.html index 981bd649..4b32da87 100644 --- a/templates/pkgbase/merge.html +++ b/templates/pkgbase/merge.html @@ -52,6 +52,7 @@

    diff --git a/templates/pkgbase/request.html b/templates/pkgbase/request.html index 61654a49..1e3d8c19 100644 --- a/templates/pkgbase/request.html +++ b/templates/pkgbase/request.html @@ -64,13 +64,14 @@

    + rows="5" cols="50" + maxlength="{{ max_chars_comment }}">{{ comments or '' }}

    {{ - "By submitting a deletion request, you ask a Trusted " - "User to delete the package base. This type of " + "By submitting a deletion request, you ask a Package " + "Maintainer to delete the package base. This type of " "request should be used for duplicates, software " "abandoned by upstream, as well as illegal and " "irreparably broken packages." | tr @@ -79,8 +80,8 @@

    +

    {{ "Requests" | tr }}

    +

    {{ "Total Statistics" | tr }}

    + + + + + + + + + + + + + + + + + + + + + + + +
    {{ "Total" | tr }}:{{ total_requests }}
    {{ "Pending" | tr }}:{{ pending_requests }}
    {{ "Closed" | tr }}:{{ closed_requests }}
    {{ "Accepted" | tr }}:{{ accepted_requests }}
    {{ "Rejected" | tr }}:{{ rejected_requests }}
    +

    {{ "Filters" | tr }}

    +
    +
    +
    + {{ "Select filter criteria" | tr }} +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + +
    +
    +
    +
    +
    {% if not total %}

    {{ "No requests matched your search criteria." | tr }}

    @@ -39,15 +109,27 @@ {{ result.RequestType.name_display() | tr }} {# If the RequestType is a merge and request.MergeBaseName is valid... #} {% if result.RequestType.ID == 3 and result.MergeBaseName %} - ({{ result.MergeBaseName }}) + + ({{ result.MergeBaseName }}) + {% endif %} {# Comments #} {{ result.Comments }} {# Filed by #} - - {{ result.User.Username }} + {# If the record has an associated User, display a link to that user. #} + {# Otherwise, display "(deleted)". #} + {% if result.User %} + + {{ result.User.Username }} + + {% else %} + (deleted) + {% endif %} +   + + (PRQ#{{ result.ID }}) {% set idle_time = config_getint("options", "request_idle_time") %} @@ -100,7 +182,7 @@ {{ "Locked" | tr }} ({{ time_left_fmt }}) {% else %} - {# Only elevated users (TU or Dev) are allowed to accept requests. #} + {# Only elevated users (PM or Dev) are allowed to accept requests. #} {{ "Accept" | tr }} @@ -123,4 +205,7 @@ {% include "partials/pager.html" %} {% endif %}
    + + + {% endblock %} diff --git a/templates/requests/close.html b/templates/requests/close.html index df767eae..ee177811 100644 --- a/templates/requests/close.html +++ b/templates/requests/close.html @@ -26,7 +26,8 @@

    + rows="5" cols="50" maxlength="{{ max_chars_comment }}" + >

    diff --git a/templates/tu/index.html b/templates/tu/index.html deleted file mode 100644 index 5060e1f7..00000000 --- a/templates/tu/index.html +++ /dev/null @@ -1,35 +0,0 @@ -{% extends "partials/layout.html" %} - -{% block pageContent %} - {% - with table_class = "current-votes", - total_votes = current_votes_count, - results = current_votes, - off_param = "coff", - by_param = "cby", - by_next = current_by_next, - title = "Current Votes", - off = current_off, - by = current_by - %} - {% include "partials/tu/proposals.html" %} - {% endwith %} - - {% - with table_class = "past-votes", - total_votes = past_votes_count, - results = past_votes, - off_param = "poff", - by_param = "pby", - by_next = past_by_next, - title = "Past Votes", - off = past_off, - by = past_by - %} - {% include "partials/tu/proposals.html" %} - {% endwith %} - - {% with title = "Last Votes by TU", votes = last_votes_by_tu %} - {% include "partials/tu/last_votes.html" %} - {% endwith %} -{% endblock %} diff --git a/test/conftest.py b/test/conftest.py index 283c979a..da24ed81 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -37,15 +37,14 @@ then clears the database for each test function run in that module. It is done this way because migration has a large cost; migrating ahead of each function takes too long when compared to this method. """ + import os import pathlib - from multiprocessing import Lock import py import pytest - -from posix_ipc import O_CREAT, Semaphore +from prometheus_client import values from sqlalchemy import create_engine from sqlalchemy.engine import URL from sqlalchemy.engine.base import Engine @@ -54,17 +53,19 @@ from sqlalchemy.orm import scoped_session import aurweb.config import aurweb.db - -from aurweb import initdb, logging, testing +from aurweb import aur_logging, initdb, testing from aurweb.testing.email import Email -from aurweb.testing.filelock import FileLock from aurweb.testing.git import GitRepository +from aurweb.testing.prometheus import clear_metrics -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) # Synchronization lock for database setup. setup_lock = Lock() +# Disable prometheus multiprocess mode for tests +values.ValueClass = values.MutexValue + def test_engine() -> Engine: """ @@ -78,13 +79,10 @@ def test_engine() -> Engine: unix_socket = aurweb.config.get_with_fallback("database", "socket", None) kwargs = { "username": aurweb.config.get("database", "user"), - "password": aurweb.config.get_with_fallback( - "database", "password", None), + "password": aurweb.config.get_with_fallback("database", "password", None), "host": aurweb.config.get("database", "host"), "port": aurweb.config.get_with_fallback("database", "port", None), - "query": { - "unix_socket": unix_socket - } + "query": {"unix_socket": unix_socket}, } backend = aurweb.config.get("database", "backend") @@ -99,6 +97,7 @@ class AlembicArgs: This structure is needed to pass conftest-specific arguments to initdb.run duration database creation. """ + verbose = False use_alembic = True @@ -138,42 +137,26 @@ def _drop_database(engine: Engine, dbname: str) -> None: def setup_email(): - # TODO: Fix this data race! This try/catch is ugly; why is it even - # racing here? Perhaps we need to multiproc + multithread lock - # inside of setup_database to block the check? - with Semaphore("/test-emails", flags=O_CREAT, initial_value=1): - if not os.path.exists(Email.TEST_DIR): - # Create the directory. - os.makedirs(Email.TEST_DIR) + if not os.path.exists(Email.TEST_DIR): + # Create the directory. + os.makedirs(Email.TEST_DIR, exist_ok=True) - # Cleanup all email files for this test suite. - prefix = Email.email_prefix(suite=True) - files = os.listdir(Email.TEST_DIR) - for file in files: - if file.startswith(prefix): - os.remove(os.path.join(Email.TEST_DIR, file)) + # Cleanup all email files for this test suite. + prefix = Email.email_prefix(suite=True) + files = os.listdir(Email.TEST_DIR) + for file in files: + if file.startswith(prefix): + os.remove(os.path.join(Email.TEST_DIR, file)) @pytest.fixture(scope="module") def setup_database(tmp_path_factory: pathlib.Path, worker_id: str) -> None: - """ Create and drop a database for the suite this fixture is used in. """ + """Create and drop a database for the suite this fixture is used in.""" engine = test_engine() dbname = aurweb.db.name() - if worker_id == "master": # pragma: no cover - # If we're not running tests through multiproc pytest-xdist. - setup_email() - yield _create_database(engine, dbname) - _drop_database(engine, dbname) - return - - def setup(path): - setup_email() - _create_database(engine, dbname) - - tmpdir = tmp_path_factory.getbasetemp().parent - file_lock = FileLock(tmpdir, dbname) - file_lock.lock(on_create=setup) + setup_email() + _create_database(engine, dbname) yield # Run the test function depending on this fixture. _drop_database(engine, dbname) # Cleanup the database. @@ -229,3 +212,13 @@ def email_test() -> None: that we set them up to be used via aurweb.testing.email.Email. """ setup_email() + + +@pytest.fixture +def prometheus_test(): + """ + Prometheus test fixture + + Removes any existing values from our metrics + """ + clear_metrics() diff --git a/test/setup.sh b/test/setup.sh index 232c33b7..33238533 100644 --- a/test/setup.sh +++ b/test/setup.sh @@ -11,7 +11,7 @@ GIT_AUTH="$TOPLEVEL/aurweb/git/auth.py" GIT_SERVE="$TOPLEVEL/aurweb/git/serve.py" GIT_UPDATE="$TOPLEVEL/aurweb/git/update.py" MKPKGLISTS="$TOPLEVEL/aurweb/scripts/mkpkglists.py" -TUVOTEREMINDER="$TOPLEVEL/aurweb/scripts/tuvotereminder.py" +VOTEREMINDER="$TOPLEVEL/aurweb/scripts/votereminder.py" PKGMAINT="$TOPLEVEL/aurweb/scripts/pkgmaint.py" USERMAINT="$TOPLEVEL/aurweb/scripts/usermaint.py" AURBLUP="$TOPLEVEL/aurweb/scripts/aurblup.py" @@ -34,6 +34,7 @@ aurwebdir = $TOPLEVEL aur_location = https://aur.archlinux.org aur_request_ml = aur-requests@lists.archlinux.org enable-maintenance = 0 +default_timezone = UTC maintenance-exceptions = 127.0.0.1 commit_uri = https://aur.archlinux.org/cgit/aur.git/log/?h=%s&id=%s localedir = $TOPLEVEL/web/locale/ @@ -55,7 +56,7 @@ ssh-options = restrict repo-path = ./aur.git/ repo-regex = [a-z0-9][a-z0-9.+_-]*$ git-shell-cmd = ./git-shell.sh -git-update-cmd = ./update.sh +git-update-cmd = $GIT_UPDATE ssh-cmdline = ssh aur@aur.archlinux.org [update] @@ -89,13 +90,6 @@ echo $GIT_NAMESPACE EOF chmod +x git-shell.sh -cat >update.sh <<-\EOF -#!/bin/sh -echo $AUR_USER -echo $AUR_PKGBASE -EOF -chmod +x update.sh - AUR_CONFIG=config export AUR_CONFIG @@ -104,9 +98,9 @@ AUTH_KEYTYPE_USER=ssh-rsa AUTH_KEYTEXT_USER=AAAAB3NzaC1yc2EAAAADAQABAAABAQCeUafDK4jqUiRHNQfwHcYjBKLZ4Rc1sNUofHApBP6j91nIvDHZe2VUqeBmFUhBz7kXK4VbXD9nlHMun2HeshL8hXnMzymZ8Wk7+IKefj61pajJkIdttw9Tnayfg7uhg5RbFy9zpEjmGjnIVjSzOXKCwppNT+CNujpKM5FD8gso/Z+l3fD+IwrPwS1SzF1Z99nqI9n2FM/JWZqluvTqnW9WdAvBDfutXxp0R5ZiLI5TAKL2Ssp5rpL70pkLXhv+9sK545zKKlXUFmw6Pi2iVBdqdRsk9ocl49dLiNIh8CYDCO3CRQn+8EnpBhTor2TKQxGJI3mzoBwWJJxoKhD/XlYJ AUTH_FINGERPRINT_USER=SHA256:F/OFtYAy0JCytAGUi4RUZnOsThhQtFMK7fH1YvFBCpo -AUTH_KEYTYPE_TU=ssh-rsa -AUTH_KEYTEXT_TU=AAAAB3NzaC1yc2EAAAADAQABAAABAQC4Q2Beg6jf2r1LZ4vwT5y10dK8+/c5RaNyTwv77wF2OSLXh32xW0ovhE2lW2gqoakdGsxgM2fTtqMTl29WOsAxlGF7x9XbWhFXFUT88Daq1fAeuihkiRjfBbInSW/WcrFZ+biLBch67addtfkkd4PmAafDeeCtszAXqza+ltBG1oxAGiTXgI3LOhA1/GtLLxsi5sPUO3ZlhvwDn4Sy0aXYx8l9hop/PU4Cjn82hyRa9r+SRxQ3KtjKxcVMnZ8IyXOrBwXTukgSBR/6nSdEmO0JPkYUFuNwh3UGFKuNkrPguL5T+4YDym6czYmZJzQ7NNl2pLKYmYgBwBe5rORlWfN5 -AUTH_FINGERPRINT_TU=SHA256:xQGC6j/U1Q3NDXLl04pm+Shr1mjYUXbGMUzlm9vby4k +AUTH_KEYTYPE_PM=ssh-rsa +AUTH_KEYTEXT_PM=AAAAB3NzaC1yc2EAAAADAQABAAABAQC4Q2Beg6jf2r1LZ4vwT5y10dK8+/c5RaNyTwv77wF2OSLXh32xW0ovhE2lW2gqoakdGsxgM2fTtqMTl29WOsAxlGF7x9XbWhFXFUT88Daq1fAeuihkiRjfBbInSW/WcrFZ+biLBch67addtfkkd4PmAafDeeCtszAXqza+ltBG1oxAGiTXgI3LOhA1/GtLLxsi5sPUO3ZlhvwDn4Sy0aXYx8l9hop/PU4Cjn82hyRa9r+SRxQ3KtjKxcVMnZ8IyXOrBwXTukgSBR/6nSdEmO0JPkYUFuNwh3UGFKuNkrPguL5T+4YDym6czYmZJzQ7NNl2pLKYmYgBwBe5rORlWfN5 +AUTH_FINGERPRINT_PM=SHA256:xQGC6j/U1Q3NDXLl04pm+Shr1mjYUXbGMUzlm9vby4k AUTH_KEYTYPE_MISSING=sha-rsa AUTH_KEYTEXT_MISSING=AAAAB3NzaC1yc2EAAAADAQABAAABAQC9UTpssBunuTBCT3KFtv+yb+cN0VmI2C9O9U7wHlkEZWxNBK8is6tnDHXBxRuvRk0LHILkTidLLFX22ZF0+TFgSz7uuEvGZVNpa2Fn2+vKJJYMvZEvb/f8VHF5/Jddt21VOyu23royTN/duiT7WIZdCtEmq5C9Y43NPfsB8FbUc+FVSYT2Lq7g1/bzvFF+CZxwCrGjC3qC7p3pshICfFR8bbWgRN33ClxIQ7MvkcDtfNu38dLotJqdfEa7NdQgba5/S586f1A4OWKc/mQJFyTaGhRBxw/cBSjqonvO0442VYLHFxlrTHoUunKyOJ8+BJfKgjWmfENC9ESY3mL/IEn5 @@ -122,17 +116,17 @@ export SSH_CLIENT SSH_CONNECTION SSH_TTY python -m aurweb.initdb --no-alembic echo "INSERT INTO Users (ID, UserName, Passwd, Email, LangPreference, AccountTypeID) VALUES (1, 'user', '!', 'user@localhost', 'en', 1);" | sqlite3 aur.db -echo "INSERT INTO Users (ID, UserName, Passwd, Email, LangPreference, AccountTypeID) VALUES (2, 'tu', '!', 'tu@localhost', 'en', 2);" | sqlite3 aur.db +echo "INSERT INTO Users (ID, UserName, Passwd, Email, LangPreference, AccountTypeID) VALUES (2, 'pm', '!', 'pm@localhost', 'en', 2);" | sqlite3 aur.db echo "INSERT INTO Users (ID, UserName, Passwd, Email, LangPreference, AccountTypeID) VALUES (3, 'dev', '!', 'dev@localhost', 'en', 3);" | sqlite3 aur.db echo "INSERT INTO Users (ID, UserName, Passwd, Email, LangPreference, AccountTypeID) VALUES (4, 'user2', '!', 'user2@localhost', 'en', 1);" | sqlite3 aur.db echo "INSERT INTO Users (ID, UserName, Passwd, Email, LangPreference, AccountTypeID) VALUES (5, 'user3', '!', 'user3@localhost', 'en', 1);" | sqlite3 aur.db echo "INSERT INTO Users (ID, UserName, Passwd, Email, LangPreference, AccountTypeID) VALUES (6, 'user4', '!', 'user4@localhost', 'en', 1);" | sqlite3 aur.db -echo "INSERT INTO Users (ID, UserName, Passwd, Email, LangPreference, AccountTypeID) VALUES (7, 'tu2', '!', 'tu2@localhost', 'en', 2);" | sqlite3 aur.db -echo "INSERT INTO Users (ID, UserName, Passwd, Email, LangPreference, AccountTypeID) VALUES (8, 'tu3', '!', 'tu3@localhost', 'en', 2);" | sqlite3 aur.db -echo "INSERT INTO Users (ID, UserName, Passwd, Email, LangPreference, AccountTypeID) VALUES (9, 'tu4', '!', 'tu4@localhost', 'en', 2);" | sqlite3 aur.db +echo "INSERT INTO Users (ID, UserName, Passwd, Email, LangPreference, AccountTypeID) VALUES (7, 'pm2', '!', 'pm2@localhost', 'en', 2);" | sqlite3 aur.db +echo "INSERT INTO Users (ID, UserName, Passwd, Email, LangPreference, AccountTypeID) VALUES (8, 'pm3', '!', 'pm3@localhost', 'en', 2);" | sqlite3 aur.db +echo "INSERT INTO Users (ID, UserName, Passwd, Email, LangPreference, AccountTypeID) VALUES (9, 'pm4', '!', 'pm4@localhost', 'en', 2);" | sqlite3 aur.db echo "INSERT INTO SSHPubKeys (UserID, Fingerprint, PubKey) VALUES (1, '$AUTH_FINGERPRINT_USER', '$AUTH_KEYTYPE_USER $AUTH_KEYTEXT_USER');" | sqlite3 aur.db -echo "INSERT INTO SSHPubKeys (UserID, Fingerprint, PubKey) VALUES (2, '$AUTH_FINGERPRINT_TU', '$AUTH_KEYTYPE_TU $AUTH_KEYTEXT_TU');" | sqlite3 aur.db +echo "INSERT INTO SSHPubKeys (UserID, Fingerprint, PubKey) VALUES (2, '$AUTH_FINGERPRINT_PM', '$AUTH_KEYTYPE_PM $AUTH_KEYTEXT_PM');" | sqlite3 aur.db echo "INSERT INTO Bans (IPAddress, BanTS) VALUES ('1.3.3.7', 0);" | sqlite3 aur.db @@ -229,5 +223,40 @@ export GIT_COMMITTER_EMAIL GIT_COMMITTER_NAME git add PKGBUILD .SRCINFO git commit -q -m 'Initial import' + git checkout -q --orphan refs/namespaces/forbidden/refs/heads/master + + cat >PKGBUILD <<-EOF + pkgname=foobar3 + pkgver=1 + pkgrel=1 + pkgdesc='aurweb test package.' + url='https://aur.archlinux.org/' + license=('MIT') + arch=('any') + depends=('python-pygit2') + source=() + md5sums=() + + package() { + echo 'Hello world!' + } + EOF + + cat >.SRCINFO <<-EOF + pkgbase = forbidden + pkgdesc = aurweb test package. + pkgver = 1 + pkgrel = 1 + url = https://aur.archlinux.org/ + arch = any + license = MIT + depends = python-pygit2 + + pkgname = foobar3 + EOF + + git add PKGBUILD .SRCINFO + git commit -q -m 'Initial import' + git checkout -q refs/namespaces/foobar/refs/heads/master ) diff --git a/test/t1100-git-auth.t b/test/t1100-git-auth.t index e29bccdd..b968f9bb 100755 --- a/test/t1100-git-auth.t +++ b/test/t1100-git-auth.t @@ -11,9 +11,9 @@ test_expect_success 'Test basic authentication.' ' grep -q AUR_PRIVILEGED=0 out ' -test_expect_success 'Test Trusted User authentication.' ' - cover "$GIT_AUTH" "$AUTH_KEYTYPE_TU" "$AUTH_KEYTEXT_TU" >out && - grep -q AUR_USER=tu out && +test_expect_success 'Test Package Maintainer authentication.' ' + cover "$GIT_AUTH" "$AUTH_KEYTYPE_PM" "$AUTH_KEYTEXT_PM" >out && + grep -q AUR_USER=pm out && grep -q AUR_PRIVILEGED=1 out ' diff --git a/test/t1200-git-serve.t b/test/t1200-git-serve.t index f1657412..a2566bd4 100755 --- a/test/t1200-git-serve.t +++ b/test/t1200-git-serve.t @@ -38,7 +38,7 @@ test_expect_success 'Test IP address logging.' ' cat >expected <<-EOF && 1.2.3.4 EOF - echo "SELECT LastSSHLoginIPAddress FROM Users WHERE UserName = \"user\";" | \ + echo "SELECT LastSSHLoginIPAddress FROM Users WHERE UserName = '"'"'user'"'"';" | \ sqlite3 aur.db >actual && test_cmp expected actual ' @@ -56,11 +56,16 @@ test_expect_success 'Test IP address bans.' ' SSH_CLIENT="$SSH_CLIENT_ORIG" ' -test_expect_success 'Test setup-repo and list-repos.' ' - SSH_ORIGINAL_COMMAND="setup-repo foobar" AUR_USER=user \ - cover "$GIT_SERVE" 2>&1 && - SSH_ORIGINAL_COMMAND="setup-repo foobar2" AUR_USER=tu \ - cover "$GIT_SERVE" 2>&1 && +test_expect_success 'Test list-repos.' ' + # insert our test packages + echo "INSERT INTO PackageBases (Name, SubmittedTS, \ + ModifiedTS, SubmitterUID, MaintainerUID, FlaggerComment) \ + VALUES ('"'"'foobar'"'"', 0, 0, 1, 1, '"'"''"'"');" | \ + sqlite3 aur.db + echo "INSERT INTO PackageBases (Name, SubmittedTS, \ + ModifiedTS, SubmitterUID, MaintainerUID, FlaggerComment) \ + VALUES ('"'"'foobar2'"'"', 0, 0, 2, 2, '"'"''"'"');" | \ + sqlite3 aur.db cat >expected <<-EOF && *foobar EOF @@ -119,27 +124,34 @@ test_expect_success "Try to push to someone else's repository." ' cover "$GIT_SERVE" 2>&1 ' -test_expect_success "Try to push to someone else's repository as Trusted User." ' +test_expect_success "Try to push to someone else's repository as Package Maintainer." ' cat >expected <<-EOF && - tu + pm foobar foobar EOF SSH_ORIGINAL_COMMAND="git-receive-pack /foobar.git/" \ - AUR_USER=tu AUR_PRIVILEGED=1 \ + AUR_USER=pm AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual ' test_expect_success "Test restore." ' - echo "DELETE FROM PackageBases WHERE Name = \"foobar\";" | \ + # Delete from DB + echo "DELETE FROM PackageBases WHERE Name = '"'"'foobar'"'"';" | \ sqlite3 aur.db && - cat >expected <<-EOF && - user - foobar - EOF + # "Create branch" as if it had been there + new=$(git -C aur.git rev-parse HEAD^) && + echo $new > aur.git/.git/refs/heads/foobar && + # Restore deleted package SSH_ORIGINAL_COMMAND="restore foobar" AUR_USER=user AUR_PRIVILEGED=0 \ - cover "$GIT_SERVE" 2>&1 >actual + cover "$GIT_SERVE" 2>&1 && + # We should find foobar with a new ID (3) in the DB after restore + echo "SELECT ID FROM PackageBases WHERE Name = '"'"'foobar'"'"';" | \ + sqlite3 aur.db >actual && + cat >expected <<-EOF && + 3 + EOF test_cmp expected actual ' @@ -151,16 +163,16 @@ test_expect_success "Try to restore an existing package base." ' ' test_expect_success "Disown all package bases." ' - SSH_ORIGINAL_COMMAND="disown foobar" AUR_USER=tu AUR_PRIVILEGED=1 \ + SSH_ORIGINAL_COMMAND="disown foobar" AUR_USER=pm AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 && - SSH_ORIGINAL_COMMAND="disown foobar2" AUR_USER=tu AUR_PRIVILEGED=1 \ + SSH_ORIGINAL_COMMAND="disown foobar2" AUR_USER=pm AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=user AUR_PRIVILEGED=0 \ cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual && - SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=tu AUR_PRIVILEGED=1 \ + SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=pm AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual ' @@ -169,7 +181,7 @@ test_expect_success "Adopt a package base as a regular user." ' SSH_ORIGINAL_COMMAND="adopt foobar" AUR_USER=user AUR_PRIVILEGED=0 \ cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && - *foobar + foobar EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=user AUR_PRIVILEGED=0 \ cover "$GIT_SERVE" 2>&1 >actual && @@ -183,13 +195,13 @@ test_expect_success "Adopt an already adopted package base." ' cover "$GIT_SERVE" 2>&1 ' -test_expect_success "Adopt a package base as a Trusted User." ' - SSH_ORIGINAL_COMMAND="adopt foobar2" AUR_USER=tu AUR_PRIVILEGED=1 \ +test_expect_success "Adopt a package base as a Package Maintainer." ' + SSH_ORIGINAL_COMMAND="adopt foobar2" AUR_USER=pm AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && *foobar2 EOF - SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=tu AUR_PRIVILEGED=1 \ + SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=pm AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual ' @@ -204,18 +216,18 @@ test_expect_success "Disown one's own package base as a regular user." ' test_cmp expected actual ' -test_expect_success "Disown one's own package base as a Trusted User." ' - SSH_ORIGINAL_COMMAND="disown foobar2" AUR_USER=tu AUR_PRIVILEGED=1 \ +test_expect_success "Disown one's own package base as a Package Maintainer." ' + SSH_ORIGINAL_COMMAND="disown foobar2" AUR_USER=pm AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && EOF - SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=tu AUR_PRIVILEGED=1 \ + SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=pm AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual ' test_expect_success "Try to steal another user's package as a regular user." ' - SSH_ORIGINAL_COMMAND="adopt foobar2" AUR_USER=tu AUR_PRIVILEGED=1 \ + SSH_ORIGINAL_COMMAND="adopt foobar2" AUR_USER=pm AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 && test_must_fail \ env SSH_ORIGINAL_COMMAND="adopt foobar2" \ @@ -229,17 +241,17 @@ test_expect_success "Try to steal another user's package as a regular user." ' cat >expected <<-EOF && *foobar2 EOF - SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=tu AUR_PRIVILEGED=1 \ + SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=pm AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual && - SSH_ORIGINAL_COMMAND="disown foobar2" AUR_USER=tu AUR_PRIVILEGED=1 \ + SSH_ORIGINAL_COMMAND="disown foobar2" AUR_USER=pm AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 ' -test_expect_success "Try to steal another user's package as a Trusted User." ' +test_expect_success "Try to steal another user's package as a Package Maintainer." ' SSH_ORIGINAL_COMMAND="adopt foobar" AUR_USER=user AUR_PRIVILEGED=0 \ cover "$GIT_SERVE" 2>&1 && - SSH_ORIGINAL_COMMAND="adopt foobar" AUR_USER=tu AUR_PRIVILEGED=1 \ + SSH_ORIGINAL_COMMAND="adopt foobar" AUR_USER=pm AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && EOF @@ -247,17 +259,17 @@ test_expect_success "Try to steal another user's package as a Trusted User." ' cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual && cat >expected <<-EOF && - *foobar + foobar EOF - SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=tu AUR_PRIVILEGED=1 \ + SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=pm AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual && - SSH_ORIGINAL_COMMAND="disown foobar" AUR_USER=tu AUR_PRIVILEGED=1 \ + SSH_ORIGINAL_COMMAND="disown foobar" AUR_USER=pm AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 ' test_expect_success "Try to disown another user's package as a regular user." ' - SSH_ORIGINAL_COMMAND="adopt foobar2" AUR_USER=tu AUR_PRIVILEGED=1 \ + SSH_ORIGINAL_COMMAND="adopt foobar2" AUR_USER=pm AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 && test_must_fail \ env SSH_ORIGINAL_COMMAND="disown foobar2" \ @@ -266,24 +278,24 @@ test_expect_success "Try to disown another user's package as a regular user." ' cat >expected <<-EOF && *foobar2 EOF - SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=tu AUR_PRIVILEGED=1 \ + SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=pm AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual && - SSH_ORIGINAL_COMMAND="disown foobar2" AUR_USER=tu AUR_PRIVILEGED=1 \ + SSH_ORIGINAL_COMMAND="disown foobar2" AUR_USER=pm AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 ' -test_expect_success "Try to disown another user's package as a Trusted User." ' +test_expect_success "Try to disown another user's package as a Package Maintainer." ' SSH_ORIGINAL_COMMAND="adopt foobar" AUR_USER=user AUR_PRIVILEGED=0 \ cover "$GIT_SERVE" 2>&1 && - SSH_ORIGINAL_COMMAND="disown foobar" AUR_USER=tu AUR_PRIVILEGED=1 \ + SSH_ORIGINAL_COMMAND="disown foobar" AUR_USER=pm AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=user AUR_PRIVILEGED=0 \ cover "$GIT_SERVE" 2>&1 >actual && test_cmp expected actual && - SSH_ORIGINAL_COMMAND="disown foobar" AUR_USER=tu AUR_PRIVILEGED=1 \ + SSH_ORIGINAL_COMMAND="disown foobar" AUR_USER=pm AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 ' @@ -335,7 +347,7 @@ test_expect_success "Disown a package base and check (co-)maintainer list." ' SSH_ORIGINAL_COMMAND="disown foobar" AUR_USER=user AUR_PRIVILEGED=0 \ cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && - *foobar + foobar EOF SSH_ORIGINAL_COMMAND="list-repos" AUR_USER=user2 AUR_PRIVILEGED=0 \ cover "$GIT_SERVE" 2>&1 >actual && @@ -350,7 +362,7 @@ test_expect_success "Disown a package base and check (co-)maintainer list." ' ' test_expect_success "Force-disown a package base and check (co-)maintainer list." ' - SSH_ORIGINAL_COMMAND="disown foobar" AUR_USER=tu AUR_PRIVILEGED=1 \ + SSH_ORIGINAL_COMMAND="disown foobar" AUR_USER=pm AUR_PRIVILEGED=1 \ cover "$GIT_SERVE" 2>&1 && cat >expected <<-EOF && EOF @@ -368,9 +380,9 @@ test_expect_success "Check whether package requests are closed when disowning." SSH_ORIGINAL_COMMAND="adopt foobar" AUR_USER=user AUR_PRIVILEGED=0 \ cover "$GIT_SERVE" 2>&1 && cat <<-EOD | sqlite3 aur.db && - INSERT INTO PackageRequests (ID, ReqTypeID, PackageBaseID, PackageBaseName, UsersID, Comments, ClosureComment) VALUES (1, 2, 3, "foobar", 4, "", ""); - INSERT INTO PackageRequests (ID, ReqTypeID, PackageBaseID, PackageBaseName, UsersID, Comments, ClosureComment) VALUES (2, 3, 3, "foobar", 5, "", ""); - INSERT INTO PackageRequests (ID, ReqTypeID, PackageBaseID, PackageBaseName, UsersID, Comments, ClosureComment) VALUES (3, 2, 2, "foobar2", 6, "", ""); + INSERT INTO PackageRequests (ID, ReqTypeID, PackageBaseID, PackageBaseName, UsersID, Comments, ClosureComment) VALUES (1, 2, 3, '"'"'foobar'"'"', 4, '"'"''"'"', '"'"''"'"'); + INSERT INTO PackageRequests (ID, ReqTypeID, PackageBaseID, PackageBaseName, UsersID, Comments, ClosureComment) VALUES (2, 3, 3, '"'"'foobar'"'"', 5, '"'"''"'"', '"'"''"'"'); + INSERT INTO PackageRequests (ID, ReqTypeID, PackageBaseID, PackageBaseName, UsersID, Comments, ClosureComment) VALUES (3, 2, 2, '"'"'foobar2'"'"', 6, '"'"''"'"', '"'"''"'"'); EOD >sendmail.out && SSH_ORIGINAL_COMMAND="disown foobar" AUR_USER=user AUR_PRIVILEGED=0 \ @@ -464,7 +476,7 @@ test_expect_success "Vote for a package base." ' cat >expected <<-EOF && 1 EOF - echo "SELECT NumVotes FROM PackageBases WHERE Name = \"foobar\";" | \ + echo "SELECT NumVotes FROM PackageBases WHERE Name = '"'"'foobar'"'"';" | \ sqlite3 aur.db >actual && test_cmp expected actual ' @@ -482,7 +494,7 @@ test_expect_success "Vote for a package base twice." ' cat >expected <<-EOF && 1 EOF - echo "SELECT NumVotes FROM PackageBases WHERE Name = \"foobar\";" | \ + echo "SELECT NumVotes FROM PackageBases WHERE Name = '"'"'foobar'"'"';" | \ sqlite3 aur.db >actual && test_cmp expected actual ' @@ -498,7 +510,7 @@ test_expect_success "Remove vote from a package base." ' cat >expected <<-EOF && 0 EOF - echo "SELECT NumVotes FROM PackageBases WHERE Name = \"foobar\";" | \ + echo "SELECT NumVotes FROM PackageBases WHERE Name = '"'"'foobar'"'"';" | \ sqlite3 aur.db >actual && test_cmp expected actual ' @@ -516,7 +528,7 @@ test_expect_success "Try to remove the vote again." ' cat >expected <<-EOF && 0 EOF - echo "SELECT NumVotes FROM PackageBases WHERE Name = \"foobar\";" | \ + echo "SELECT NumVotes FROM PackageBases WHERE Name = '"'"'foobar'"'"';" | \ sqlite3 aur.db >actual && test_cmp expected actual ' diff --git a/test/t1300-git-update.t b/test/t1300-git-update.t index e9d943c0..979cd281 100755 --- a/test/t1300-git-update.t +++ b/test/t1300-git-update.t @@ -125,14 +125,14 @@ test_expect_success 'Performing a non-fast-forward ref update.' ' test_cmp expected actual ' -test_expect_success 'Performing a non-fast-forward ref update as Trusted User.' ' +test_expect_success 'Performing a non-fast-forward ref update as Package Maintainer.' ' old=$(git -C aur.git rev-parse HEAD) && new=$(git -C aur.git rev-parse HEAD^) && cat >expected <<-EOD && error: denying non-fast-forward (you should pull first) EOD test_must_fail \ - env AUR_USER=tu AUR_PKGBASE=foobar AUR_PRIVILEGED=1 \ + env AUR_USER=pm AUR_PKGBASE=foobar AUR_PRIVILEGED=1 \ cover "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 && test_cmp expected actual ' @@ -149,10 +149,10 @@ test_expect_success 'Performing a non-fast-forward ref update as normal user wit test_cmp expected actual ' -test_expect_success 'Performing a non-fast-forward ref update as Trusted User with AUR_OVERWRITE=1.' ' +test_expect_success 'Performing a non-fast-forward ref update as Package Maintainer with AUR_OVERWRITE=1.' ' old=$(git -C aur.git rev-parse HEAD) && new=$(git -C aur.git rev-parse HEAD^) && - AUR_USER=tu AUR_PKGBASE=foobar AUR_PRIVILEGED=1 AUR_OVERWRITE=1 \ + AUR_USER=pm AUR_PKGBASE=foobar AUR_PRIVILEGED=1 AUR_OVERWRITE=1 \ cover "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 ' @@ -175,10 +175,8 @@ test_expect_success 'Removing .SRCINFO with a follow-up fix.' ' git -C aur.git commit -q -m "Remove .SRCINFO" && git -C aur.git revert --no-edit HEAD && new=$(git -C aur.git rev-parse HEAD) && - test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ - cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && - grep -q "^error: missing .SRCINFO$" actual + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 ' test_expect_success 'Removing PKGBUILD.' ' @@ -193,7 +191,7 @@ test_expect_success 'Removing PKGBUILD.' ' grep -q "^error: missing PKGBUILD$" actual ' -test_expect_success 'Pushing a tree with a subdirectory.' ' +test_expect_success 'Pushing a tree with a forbidden subdirectory.' ' old=$(git -C aur.git rev-parse HEAD) && test_when_finished "git -C aur.git reset --hard $old" && mkdir aur.git/subdir && @@ -207,17 +205,123 @@ test_expect_success 'Pushing a tree with a subdirectory.' ' grep -q "^error: the repository must not contain subdirectories$" actual ' +test_expect_success 'Pushing a tree with an allowed subdirectory for pgp keys; wrong files.' ' + old=$(git -C aur.git rev-parse HEAD) && + test_when_finished "git -C aur.git reset --hard $old" && + mkdir -p aur.git/keys/pgp/ && + touch aur.git/keys/pgp/nonsense && + git -C aur.git add keys/pgp/nonsense && + git -C aur.git commit -q -m "Add some nonsense" && + new=$(git -C aur.git rev-parse HEAD) && + test_must_fail \ + env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + grep -q "^error: the subdir may only contain .asc (PGP pub key) files$" actual +' + +test_expect_success 'Pushing a tree with an allowed subdirectory for pgp keys; another subdir.' ' + old=$(git -C aur.git rev-parse HEAD) && + test_when_finished "git -C aur.git reset --hard $old" && + mkdir -p aur.git/keys/pgp/bla/ && + touch aur.git/keys/pgp/bla/x.asc && + git -C aur.git add keys/pgp/bla/x.asc && + git -C aur.git commit -q -m "Add some nonsense" && + new=$(git -C aur.git rev-parse HEAD) && + test_must_fail \ + env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + grep -q "^error: the subdir may only contain .asc (PGP pub key) files$" actual +' + +test_expect_success 'Pushing a tree with an allowed subdirectory for pgp keys; wrong subdir.' ' + old=$(git -C aur.git rev-parse HEAD) && + test_when_finished "git -C aur.git reset --hard $old" && + mkdir -p aur.git/keys/xyz/ && + touch aur.git/keys/xyz/x.asc && + git -C aur.git add keys/xyz/x.asc && + git -C aur.git commit -q -m "Add some nonsense" && + new=$(git -C aur.git rev-parse HEAD) && + test_must_fail \ + env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + grep -q "^error: the keys/ subdir may only contain a pgp/ directory$" actual +' + +test_expect_success 'Pushing a tree with an allowed subdirectory with pgp keys; additional files' ' + old=$(git -C aur.git rev-parse HEAD) && + test_when_finished "git -C aur.git reset --hard $old" && + mkdir -p aur.git/keys/pgp/ && + touch aur.git/keys/pgp/x.asc && + touch aur.git/keys/nonsense && + git -C aur.git add keys/pgp/x.asc && + git -C aur.git add keys/nonsense && + git -C aur.git commit -q -m "Add pgp key" && + new=$(git -C aur.git rev-parse HEAD) && + test_must_fail \ + env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + grep -q "^error: the keys/ subdir may only contain a pgp/ directory$" actual +' + +test_expect_success 'Pushing a tree with an allowed subdirectory with pgp keys; additional subdir' ' + old=$(git -C aur.git rev-parse HEAD) && + test_when_finished "git -C aur.git reset --hard $old" && + mkdir -p aur.git/keys/pgp/ && + mkdir -p aur.git/somedir/ && + touch aur.git/keys/pgp/x.asc && + touch aur.git/somedir/nonsense && + git -C aur.git add keys/pgp/x.asc && + git -C aur.git add somedir/nonsense && + git -C aur.git commit -q -m "Add pgp key" && + new=$(git -C aur.git rev-parse HEAD) && + test_must_fail \ + env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + grep -q "^error: the repository must not contain subdirectories$" actual +' + +test_expect_success 'Pushing a tree with an allowed subdirectory with pgp keys; keys to large' ' + old=$(git -C aur.git rev-parse HEAD) && + test_when_finished "git -C aur.git reset --hard $old" && + mkdir -p aur.git/keys/pgp/ && + printf "%256001s" x > aur.git/keys/pgp/x.asc && + git -C aur.git add keys/pgp/x.asc && + git -C aur.git commit -q -m "Add pgp key" && + new=$(git -C aur.git rev-parse HEAD) && + test_must_fail \ + env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + grep -q "^error: maximum blob size (250.00KiB) exceeded$" actual +' + +test_expect_success 'Pushing a tree with an allowed subdirectory with pgp keys.' ' + old=$(git -C aur.git rev-parse HEAD) && + test_when_finished "git -C aur.git reset --hard $old" && + mkdir -p aur.git/keys/pgp/ && + touch aur.git/keys/pgp/x.asc && + git -C aur.git add keys/pgp/x.asc && + git -C aur.git commit -q -m "Add pgp key" && + new=$(git -C aur.git rev-parse HEAD) && + env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" 2>&1 +' + test_expect_success 'Pushing a tree with a large blob.' ' old=$(git -C aur.git rev-parse HEAD) && test_when_finished "git -C aur.git reset --hard $old" && printf "%256001s" x >aur.git/file && git -C aur.git add file && git -C aur.git commit -q -m "Add large blob" && + first_error=$(git -C aur.git rev-parse HEAD) && + touch aur.git/another.file && + git -C aur.git add another.file && + git -C aur.git commit -q -m "Add another commit" && new=$(git -C aur.git rev-parse HEAD) && test_must_fail \ env AUR_USER=user AUR_PKGBASE=foobar AUR_PRIVILEGED=0 \ cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && - grep -q "^error: maximum blob size (250.00KiB) exceeded$" actual + grep -q "^error: maximum blob size (250.00KiB) exceeded$" actual && + grep -q "^error: $first_error:$" actual ' test_expect_success 'Pushing .SRCINFO with a non-matching package base.' ' @@ -432,7 +536,23 @@ test_expect_success 'Pushing a blacklisted package.' ' test_cmp expected actual ' -test_expect_success 'Pushing a blacklisted package as Trusted User.' ' +test_expect_success 'Pushing a blacklisted pkgbase.' ' + test_when_finished "git -C aur.git checkout refs/namespaces/foobar/refs/heads/master" && + git -C aur.git checkout -q refs/namespaces/forbidden/refs/heads/master && + old=$(git -C aur.git rev-parse HEAD) && + echo " " >>aur.git/.SRCINFO && + git -C aur.git commit -q -am "Do something" && + new=$(git -C aur.git rev-parse HEAD) && + cat >expected <<-EOD && + error: pkgbase is blacklisted: forbidden + EOD + test_must_fail \ + env AUR_USER=user AUR_PKGBASE=forbidden AUR_PRIVILEGED=0 \ + cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && + test_cmp expected actual +' + +test_expect_success 'Pushing a blacklisted package as Package Maintainer.' ' old=$(git -C aur.git rev-parse HEAD) && test_when_finished "git -C aur.git reset --hard $old" && echo "pkgname = forbidden" >>aur.git/.SRCINFO && @@ -441,7 +561,7 @@ test_expect_success 'Pushing a blacklisted package as Trusted User.' ' cat >expected <<-EOD && warning: package is blacklisted: forbidden EOD - AUR_USER=tu AUR_PKGBASE=foobar AUR_PRIVILEGED=1 \ + AUR_USER=pm AUR_PKGBASE=foobar AUR_PRIVILEGED=1 \ cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && test_cmp expected actual ' @@ -461,7 +581,7 @@ test_expect_success 'Pushing a package already in the official repositories.' ' test_cmp expected actual ' -test_expect_success 'Pushing a package already in the official repositories as Trusted User.' ' +test_expect_success 'Pushing a package already in the official repositories as Package Maintainer.' ' old=$(git -C aur.git rev-parse HEAD) && test_when_finished "git -C aur.git reset --hard $old" && echo "pkgname = official" >>aur.git/.SRCINFO && @@ -470,7 +590,7 @@ test_expect_success 'Pushing a package already in the official repositories as T cat >expected <<-EOD && warning: package already provided by [core]: official EOD - AUR_USER=tu AUR_PKGBASE=foobar AUR_PRIVILEGED=1 \ + AUR_USER=pm AUR_PKGBASE=foobar AUR_PRIVILEGED=1 \ cover "$GIT_UPDATE" refs/heads/master "$old" "$new" >actual 2>&1 && test_cmp expected actual ' diff --git a/test/test_accepted_term.py b/test/test_accepted_term.py index 2af7127b..9af19105 100644 --- a/test/test_accepted_term.py +++ b/test/test_accepted_term.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -17,17 +16,21 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @pytest.fixture def term() -> Term: with db.begin(): - term = db.create(Term, Description="Test term", - URL="https://test.term") + term = db.create(Term, Description="Test term", URL="https://test.term") yield term diff --git a/test/test_account_type.py b/test/test_account_type.py index 1d71f878..4b56b7ff 100644 --- a/test/test_account_type.py +++ b/test/test_account_type.py @@ -22,26 +22,30 @@ def account_type() -> AccountType: def test_account_type(account_type): - """ Test creating an AccountType, and reading its columns. """ + """Test creating an AccountType, and reading its columns.""" # Make sure it got db.created and was given an ID. assert bool(account_type.ID) # Next, test our string functions. assert str(account_type) == "TestUser" - assert repr(account_type) == \ - "" % ( - account_type.ID) + assert repr(account_type) == "" % ( + account_type.ID + ) - record = db.query(AccountType, - AccountType.AccountType == "TestUser").first() + record = db.query(AccountType, AccountType.AccountType == "TestUser").first() assert account_type == record def test_user_account_type_relationship(account_type): with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=account_type) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountType=account_type, + ) assert user.AccountType == account_type diff --git a/test/test_accounts_routes.py b/test/test_accounts_routes.py index 37b3d130..1591d658 100644 --- a/test/test_accounts_routes.py +++ b/test/test_accounts_routes.py @@ -1,24 +1,28 @@ import re import tempfile - -from datetime import datetime +from datetime import UTC, datetime from http import HTTPStatus from logging import DEBUG from subprocess import Popen import lxml.html import pytest - from fastapi.testclient import TestClient +import aurweb.config import aurweb.models.account_type as at - -from aurweb import captcha, db, logging, time +from aurweb import aur_logging, captcha, db, time from aurweb.asgi import app from aurweb.db import create, query from aurweb.models.accepted_term import AcceptedTerm -from aurweb.models.account_type import (DEVELOPER_ID, TRUSTED_USER, TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID, USER_ID, - AccountType) +from aurweb.models.account_type import ( + DEVELOPER_ID, + PACKAGE_MAINTAINER, + PACKAGE_MAINTAINER_AND_DEV_ID, + PACKAGE_MAINTAINER_ID, + USER_ID, + AccountType, +) from aurweb.models.ban import Ban from aurweb.models.session import Session from aurweb.models.ssh_pub_key import SSHPubKey, get_fingerprint @@ -27,11 +31,14 @@ from aurweb.models.user import User from aurweb.testing.html import get_errors from aurweb.testing.requests import Request -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) # Some test global constants. TEST_USERNAME = "test" TEST_EMAIL = "test@example.org" +TEST_REFERER = { + "referer": aurweb.config.get("options", "aur_location") + "/login", +} def make_ssh_pubkey(): @@ -39,8 +46,11 @@ def make_ssh_pubkey(): # dependency to passing this test). with tempfile.TemporaryDirectory() as tmpdir: with open("/dev/null", "w") as null: - proc = Popen(["ssh-keygen", "-f", f"{tmpdir}/test.ssh", "-N", ""], - stdout=null, stderr=null) + proc = Popen( + ["ssh-keygen", "-f", f"{tmpdir}/test.ssh", "-N", ""], + stdout=null, + stderr=null, + ) proc.wait() assert proc.returncode == 0 @@ -55,14 +65,26 @@ def setup(db_test): @pytest.fixture def client() -> TestClient: - yield TestClient(app=app) + client = TestClient(app=app) + + # Necessary for forged login CSRF protection on the login route. Set here + # instead of only on the necessary requests for convenience. + client.headers.update(TEST_REFERER) + + # disable redirects for our tests + client.follow_redirects = False + yield client def create_user(username: str) -> User: email = f"{username}@example.org" - user = create(User, Username=username, Email=email, - Passwd="testPassword", - AccountTypeID=USER_ID) + user = create( + User, + Username=username, + Email=email, + Passwd="testPassword", + AccountTypeID=USER_ID, + ) return user @@ -74,9 +96,9 @@ def user() -> User: @pytest.fixture -def tu_user(user: User): +def pm_user(user: User): with db.begin(): - user.AccountTypeID = TRUSTED_USER_AND_DEV_ID + user.AccountTypeID = PACKAGE_MAINTAINER_AND_DEV_ID yield user @@ -85,8 +107,8 @@ def test_get_passreset_authed_redirects(client: TestClient, user: User): assert sid is not None with client as request: - response = request.get("/passreset", cookies={"AURSID": sid}, - allow_redirects=False) + request.cookies = {"AURSID": sid} + response = request.get("/passreset") assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/" @@ -101,7 +123,8 @@ def test_get_passreset(client: TestClient): def test_get_passreset_translation(client: TestClient): # Test that translation works; set it to de. with client as request: - response = request.get("/passreset", cookies={"AURLANG": "de"}) + request.cookies = {"AURLANG": "de"} + response = request.get("/passreset") # The header title should be translated. assert "Passwort zurücksetzen" in response.text @@ -115,12 +138,13 @@ def test_get_passreset_translation(client: TestClient): # Restore english. with client as request: - response = request.get("/passreset", cookies={"AURLANG": "en"}) + request.cookies = {"AURLANG": "en"} + response = request.get("/passreset") def test_get_passreset_with_resetkey(client: TestClient): with client as request: - response = request.get("/passreset", data={"resetkey": "abcd"}) + response = request.get("/passreset", params={"resetkey": "abcd"}) assert response.status_code == int(HTTPStatus.OK) @@ -129,10 +153,11 @@ def test_post_passreset_authed_redirects(client: TestClient, user: User): assert sid is not None with client as request: - response = request.post("/passreset", - cookies={"AURSID": sid}, - data={"user": "blah"}, - allow_redirects=False) + request.cookies = {"AURSID": sid} + response = request.post( + "/passreset", + data={"user": "blah"}, + ) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/" @@ -166,8 +191,9 @@ def test_post_passreset_user_suspended(client: TestClient, user: User): def test_post_passreset_resetkey(client: TestClient, user: User): with db.begin(): - user.session = Session(UsersID=user.ID, SessionID="blah", - LastUpdateTS=time.utcnow()) + user.session = Session( + UsersID=user.ID, SessionID="blah", LastUpdateTS=time.utcnow() + ) # Prepare a password reset. with client as request: @@ -182,7 +208,7 @@ def test_post_passreset_resetkey(client: TestClient, user: User): "user": TEST_USERNAME, "resetkey": resetkey, "password": "abcd1234", - "confirm": "abcd1234" + "confirm": "abcd1234", } with client as request: @@ -200,10 +226,7 @@ def make_resetkey(client: TestClient, user: User): def make_passreset_data(user: User, resetkey: str): - return { - "user": user.Username, - "resetkey": resetkey - } + return {"user": user.Username, "resetkey": resetkey} def test_post_passreset_error_invalid_email(client: TestClient, user: User): @@ -240,8 +263,7 @@ def test_post_passreset_error_missing_field(client: TestClient, user: User): assert error in response.content.decode("utf-8") -def test_post_passreset_error_password_mismatch(client: TestClient, - user: User): +def test_post_passreset_error_password_mismatch(client: TestClient, user: User): resetkey = make_resetkey(client, user) post_data = make_passreset_data(user, resetkey) @@ -257,8 +279,7 @@ def test_post_passreset_error_password_mismatch(client: TestClient, assert error in response.content.decode("utf-8") -def test_post_passreset_error_password_requirements(client: TestClient, - user: User): +def test_post_passreset_error_password_requirements(client: TestClient, user: User): resetkey = make_resetkey(client, user) post_data = make_passreset_data(user, resetkey) @@ -284,7 +305,7 @@ def test_get_register(client: TestClient): def post_register(request, **kwargs): - """ A simple helper that allows overrides to test defaults. """ + """A simple helper that allows overrides to test defaults.""" salt = captcha.get_captcha_salts()[0] token = captcha.get_captcha_token(salt) answer = captcha.get_captcha_answer(token) @@ -297,7 +318,7 @@ def post_register(request, **kwargs): "L": "en", "TZ": "UTC", "captcha": answer, - "captcha_salt": salt + "captcha_salt": salt, } # For any kwargs given, override their k:v pairs in data. @@ -305,7 +326,7 @@ def post_register(request, **kwargs): for k, v in args.items(): data[k] = v - return request.post("/register", data=data, allow_redirects=False) + return request.post("/register", data=data) def test_post_register(client: TestClient): @@ -370,9 +391,10 @@ def test_post_register_error_invalid_captcha(client: TestClient): def test_post_register_error_ip_banned(client: TestClient): - # 'testclient' is used as request.client.host via FastAPI TestClient. + # 'testclient' is our fallback value in case request.client is None + # which is the case for TestClient with db.begin(): - create(Ban, IPAddress="testclient", BanTS=datetime.utcnow()) + create(Ban, IPAddress="testclient", BanTS=datetime.now(UTC)) with client as request: response = post_register(request) @@ -380,9 +402,11 @@ def test_post_register_error_ip_banned(client: TestClient): assert response.status_code == int(HTTPStatus.BAD_REQUEST) content = response.content.decode() - assert ("Account registration has been disabled for your IP address, " + - "probably due to sustained spam attacks. Sorry for the " + - "inconvenience.") in content + assert ( + "Account registration has been disabled for your IP address, " + + "probably due to sustained spam attacks. Sorry for the " + + "inconvenience." + ) in content def test_post_register_error_missing_username(client: TestClient): @@ -489,7 +513,7 @@ def test_post_register_error_invalid_pgp_fingerprints(client: TestClient): expected = "The PGP key fingerprint is invalid." assert expected in content - pk = 'z' + ('a' * 39) + pk = "z" + ("a" * 39) with client as request: response = post_register(request, K=pk) @@ -569,8 +593,11 @@ def test_post_register_error_ssh_pubkey_taken(client: TestClient, user: User): # dependency to passing this test). with tempfile.TemporaryDirectory() as tmpdir: with open("/dev/null", "w") as null: - proc = Popen(["ssh-keygen", "-f", f"{tmpdir}/test.ssh", "-N", ""], - stdout=null, stderr=null) + proc = Popen( + ["ssh-keygen", "-f", f"{tmpdir}/test.ssh", "-N", ""], + stdout=null, + stderr=null, + ) proc.wait() assert proc.returncode == 0 @@ -602,8 +629,11 @@ def test_post_register_with_ssh_pubkey(client: TestClient): # dependency to passing this test). with tempfile.TemporaryDirectory() as tmpdir: with open("/dev/null", "w") as null: - proc = Popen(["ssh-keygen", "-f", f"{tmpdir}/test.ssh", "-N", ""], - stdout=null, stderr=null) + proc = Popen( + ["ssh-keygen", "-f", f"{tmpdir}/test.ssh", "-N", ""], + stdout=null, + stderr=null, + ) proc.wait() assert proc.returncode == 0 @@ -615,25 +645,38 @@ def test_post_register_with_ssh_pubkey(client: TestClient): assert response.status_code == int(HTTPStatus.OK) + # Let's create another user accidentally pasting their key twice + with db.begin(): + db.query(SSHPubKey).delete() -def test_get_account_edit_tu_as_tu(client: TestClient, tu_user: User): - """ Test edit get route of another TU as a TU. """ + pk_double = pk + "\n" + pk + with client as request: + response = post_register( + request, U="doubleKey", E="doubleKey@email.org", PK=pk_double + ) + + assert response.status_code == int(HTTPStatus.OK) + + +def test_get_account_edit_pm_as_pm(client: TestClient, pm_user: User): + """Test edit get route of another PM as a PM.""" with db.begin(): user2 = create_user("test2") - user2.AccountTypeID = at.TRUSTED_USER_ID + user2.AccountTypeID = at.PACKAGE_MAINTAINER_ID - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} endpoint = f"/account/{user2.Username}/edit" with client as request: - response = request.get(endpoint, cookies=cookies) + request.cookies = cookies + response = request.get(endpoint) assert response.status_code == int(HTTPStatus.OK) # Verify that we have an account type selection and that the - # "{at.TRUSTED_USER}" option is selected. + # "{at.PACKAGE_MAINTAINER}" option is selected. root = parse_root(response.text) atype = root.xpath('//select[@id="id_type"]/option[@selected="selected"]') - expected = at.TRUSTED_USER + expected = at.PACKAGE_MAINTAINER assert atype[0].text.strip() == expected username = root.xpath('//input[@id="id_username"]')[0] @@ -642,16 +685,17 @@ def test_get_account_edit_tu_as_tu(client: TestClient, tu_user: User): assert email.attrib["value"] == user2.Email -def test_get_account_edit_as_tu(client: TestClient, tu_user: User): - """ Test edit get route of another user as a TU. """ +def test_get_account_edit_as_pm(client: TestClient, pm_user: User): + """Test edit get route of another user as a PM.""" with db.begin(): user2 = create_user("test2") - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} endpoint = f"/account/{user2.Username}/edit" with client as request: - response = request.get(endpoint, cookies=cookies) + request.cookies = cookies + response = request.get(endpoint) assert response.status_code == int(HTTPStatus.OK) # Verify that we have an account type selection and that the @@ -669,25 +713,27 @@ def test_get_account_edit_as_tu(client: TestClient, tu_user: User): def test_get_account_edit_type(client: TestClient, user: User): - """ Test that users do not have an Account Type field. """ + """Test that users do not have an Account Type field.""" cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/account/{user.Username}/edit" with client as request: - response = request.get(endpoint, cookies=cookies) + request.cookies = cookies + response = request.get(endpoint) assert response.status_code == int(HTTPStatus.OK) assert "id_type" not in response.text -def test_get_account_edit_type_as_tu(client: TestClient, tu_user: User): +def test_get_account_edit_type_as_pm(client: TestClient, pm_user: User): with db.begin(): - user2 = create_user("test_tu") + user2 = create_user("test_pm") - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} endpoint = f"/account/{user2.Username}/edit" with client as request: - response = request.get(endpoint, cookies=cookies) + request.cookies = cookies + response = request.get(endpoint) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -700,34 +746,48 @@ def test_get_account_edit_unauthorized(client: TestClient, user: User): sid = user.login(request, "testPassword") with db.begin(): - user2 = create(User, Username="test2", Email="test2@example.org", - Passwd="testPassword", AccountTypeID=USER_ID) + user2 = create( + User, + Username="test2", + Email="test2@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) endpoint = f"/account/{user2.Username}/edit" with client as request: # Try to edit `test2` while authenticated as `test`. - response = request.get(endpoint, cookies={"AURSID": sid}, - allow_redirects=False) + request.cookies = {"AURSID": sid} + response = request.get(endpoint) assert response.status_code == int(HTTPStatus.SEE_OTHER) expected = f"/account/{user2.Username}" assert response.headers.get("location") == expected +def test_get_account_edit_not_exists(client: TestClient, pm_user: User): + """Test that users do not have an Account Type field.""" + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} + endpoint = "/account/doesnotexist/edit" + + with client as request: + request.cookies = cookies + response = request.get(endpoint) + assert response.status_code == int(HTTPStatus.NOT_FOUND) + + def test_post_account_edit(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") - post_data = { - "U": "test", - "E": "test666@example.org", - "passwd": "testPassword" - } + post_data = {"U": "test", "E": "test666@example.org", "passwd": "testPassword"} with client as request: - response = request.post("/account/test/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + request.cookies = {"AURSID": sid} + response = request.post( + "/account/test/edit", + data=post_data, + ) assert response.status_code == int(HTTPStatus.OK) @@ -736,11 +796,11 @@ def test_post_account_edit(client: TestClient, user: User): assert expected in response.content.decode() -def test_post_account_edit_type_as_tu(client: TestClient, tu_user: User): +def test_post_account_edit_type_as_pm(client: TestClient, pm_user: User): with db.begin(): - user2 = create_user("test_tu") + user2 = create_user("test_pm") - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} endpoint = f"/account/{user2.Username}/edit" data = { "U": user2.Username, @@ -749,16 +809,17 @@ def test_post_account_edit_type_as_tu(client: TestClient, tu_user: User): "passwd": "testPassword", } with client as request: - resp = request.post(endpoint, data=data, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.OK) -def test_post_account_edit_type_as_dev(client: TestClient, tu_user: User): +def test_post_account_edit_type_as_dev(client: TestClient, pm_user: User): with db.begin(): user2 = create_user("test2") - tu_user.AccountTypeID = at.DEVELOPER_ID + pm_user.AccountTypeID = at.DEVELOPER_ID - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} endpoint = f"/account/{user2.Username}/edit" data = { "U": user2.Username, @@ -767,18 +828,18 @@ def test_post_account_edit_type_as_dev(client: TestClient, tu_user: User): "passwd": "testPassword", } with client as request: - resp = request.post(endpoint, data=data, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.OK) assert user2.AccountTypeID == at.DEVELOPER_ID -def test_post_account_edit_invalid_type_as_tu(client: TestClient, - tu_user: User): +def test_post_account_edit_invalid_type_as_pm(client: TestClient, pm_user: User): with db.begin(): - user2 = create_user("test_tu") - tu_user.AccountTypeID = at.TRUSTED_USER_ID + user2 = create_user("test_pm") + pm_user.AccountTypeID = at.PACKAGE_MAINTAINER_ID - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} endpoint = f"/account/{user2.Username}/edit" data = { "U": user2.Username, @@ -787,36 +848,35 @@ def test_post_account_edit_invalid_type_as_tu(client: TestClient, "passwd": "testPassword", } with client as request: - resp = request.post(endpoint, data=data, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) assert user2.AccountTypeID == at.USER_ID errors = get_errors(resp.text) - expected = ("You do not have permission to change this user's " - f"account type to {at.DEVELOPER}.") + expected = ( + "You do not have permission to change this user's " + f"account type to {at.DEVELOPER}." + ) assert errors[0].text.strip() == expected -def test_post_account_edit_dev(client: TestClient, tu_user: User): - # Modify our user to be a "Trusted User & Developer" - name = "Trusted User & Developer" - tu_or_dev = query(AccountType, AccountType.AccountType == name).first() +def test_post_account_edit_dev(client: TestClient, pm_user: User): + # Modify our user to be a "Package Maintainer & Developer" + name = "Package Maintainer & Developer" + pm_or_dev = query(AccountType, AccountType.AccountType == name).first() with db.begin(): - user.AccountType = tu_or_dev + user.AccountType = pm_or_dev request = Request() - sid = tu_user.login(request, "testPassword") + sid = pm_user.login(request, "testPassword") - post_data = { - "U": "test", - "E": "test666@example.org", - "passwd": "testPassword" - } + post_data = {"U": "test", "E": "test666@example.org", "passwd": "testPassword"} - endpoint = f"/account/{tu_user.Username}/edit" + endpoint = f"/account/{pm_user.Username}/edit" with client as request: - response = request.post(endpoint, cookies={"AURSID": sid}, - data=post_data, allow_redirects=False) + request.cookies = {"AURSID": sid} + response = request.post(endpoint, data=post_data) assert response.status_code == int(HTTPStatus.OK) expected = "The account, test, " @@ -824,6 +884,19 @@ def test_post_account_edit_dev(client: TestClient, tu_user: User): assert expected in response.content.decode() +def test_post_account_edit_not_exists(client: TestClient, pm_user: User): + request = Request() + sid = pm_user.login(request, "testPassword") + + post_data = {"U": "test", "E": "test666@example.org", "passwd": "testPassword"} + + endpoint = "/account/doesnotexist/edit" + with client as request: + request.cookies = {"AURSID": sid} + response = request.post(endpoint, data=post_data) + assert response.status_code == int(HTTPStatus.NOT_FOUND) + + def test_post_account_edit_language(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") @@ -832,13 +905,15 @@ def test_post_account_edit_language(client: TestClient, user: User): "U": "test", "E": "test@example.org", "L": "de", # German - "passwd": "testPassword" + "passwd": "testPassword", } with client as request: - response = request.post("/account/test/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + request.cookies = {"AURSID": sid} + response = request.post( + "/account/test/edit", + data=post_data, + ) assert response.status_code == int(HTTPStatus.OK) @@ -859,33 +934,31 @@ def test_post_account_edit_timezone(client: TestClient, user: User): "U": "test", "E": "test@example.org", "TZ": "CET", - "passwd": "testPassword" + "passwd": "testPassword", } with client as request: - response = request.post("/account/test/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + request.cookies = {"AURSID": sid} + response = request.post( + "/account/test/edit", + data=post_data, + ) assert response.status_code == int(HTTPStatus.OK) -def test_post_account_edit_error_missing_password(client: TestClient, - user: User): +def test_post_account_edit_error_missing_password(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") - post_data = { - "U": "test", - "E": "test@example.org", - "TZ": "CET", - "passwd": "" - } + post_data = {"U": "test", "E": "test@example.org", "TZ": "CET", "passwd": ""} with client as request: - response = request.post("/account/test/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + request.cookies = {"AURSID": sid} + response = request.post( + "/account/test/edit", + data=post_data, + ) assert response.status_code == int(HTTPStatus.BAD_REQUEST) @@ -893,22 +966,18 @@ def test_post_account_edit_error_missing_password(client: TestClient, assert "Invalid password." in content -def test_post_account_edit_error_invalid_password(client: TestClient, - user: User): +def test_post_account_edit_error_invalid_password(client: TestClient, user: User): request = Request() sid = user.login(request, "testPassword") - post_data = { - "U": "test", - "E": "test@example.org", - "TZ": "CET", - "passwd": "invalid" - } + post_data = {"U": "test", "E": "test@example.org", "TZ": "CET", "passwd": "invalid"} with client as request: - response = request.post("/account/test/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + request.cookies = {"AURSID": sid} + response = request.post( + "/account/test/edit", + data=post_data, + ) assert response.status_code == int(HTTPStatus.BAD_REQUEST) @@ -916,18 +985,17 @@ def test_post_account_edit_error_invalid_password(client: TestClient, assert "Invalid password." in content -def test_post_account_edit_suspend_unauthorized(client: TestClient, - user: User): +def test_post_account_edit_suspend_unauthorized(client: TestClient, user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} post_data = { "U": "test", "E": "test@example.org", "S": True, - "passwd": "testPassword" + "passwd": "testPassword", } with client as request: - resp = request.post(f"/account/{user.Username}/edit", data=post_data, - cookies=cookies) + request.cookies = cookies + resp = request.post(f"/account/{user.Username}/edit", data=post_data) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) @@ -937,7 +1005,7 @@ def test_post_account_edit_suspend_unauthorized(client: TestClient, def test_post_account_edit_inactivity(client: TestClient, user: User): with db.begin(): - user.AccountTypeID = TRUSTED_USER_ID + user.AccountTypeID = PACKAGE_MAINTAINER_ID assert not user.Suspended cookies = {"AURSID": user.login(Request(), "testPassword")} @@ -945,11 +1013,11 @@ def test_post_account_edit_inactivity(client: TestClient, user: User): "U": "test", "E": "test@example.org", "J": True, - "passwd": "testPassword" + "passwd": "testPassword", } with client as request: - resp = request.post(f"/account/{user.Username}/edit", data=post_data, - cookies=cookies) + request.cookies = cookies + resp = request.post(f"/account/{user.Username}/edit", data=post_data) assert resp.status_code == int(HTTPStatus.OK) # Make sure the user record got updated correctly. @@ -957,8 +1025,8 @@ def test_post_account_edit_inactivity(client: TestClient, user: User): post_data.update({"J": False}) with client as request: - resp = request.post(f"/account/{user.Username}/edit", data=post_data, - cookies=cookies) + request.cookies = cookies + resp = request.post(f"/account/{user.Username}/edit", data=post_data) assert resp.status_code == int(HTTPStatus.OK) assert user.InactivityTS == 0 @@ -966,7 +1034,7 @@ def test_post_account_edit_inactivity(client: TestClient, user: User): def test_post_account_edit_suspended(client: TestClient, user: User): with db.begin(): - user.AccountTypeID = TRUSTED_USER_ID + user.AccountTypeID = PACKAGE_MAINTAINER_ID assert not user.Suspended cookies = {"AURSID": user.login(Request(), "testPassword")} @@ -974,22 +1042,18 @@ def test_post_account_edit_suspended(client: TestClient, user: User): "U": "test", "E": "test@example.org", "S": True, - "passwd": "testPassword" + "passwd": "testPassword", } endpoint = f"/account/{user.Username}/edit" with client as request: - resp = request.post(endpoint, data=post_data, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data=post_data) assert resp.status_code == int(HTTPStatus.OK) # Make sure the user record got updated correctly. assert user.Suspended - - post_data.update({"S": False}) - with client as request: - resp = request.post(endpoint, data=post_data, cookies=cookies) - assert resp.status_code == int(HTTPStatus.OK) - - assert not user.Suspended + # Let's make sure the DB got updated properly. + assert user.session is None def test_post_account_edit_error_unauthorized(client: TestClient, user: User): @@ -997,21 +1061,26 @@ def test_post_account_edit_error_unauthorized(client: TestClient, user: User): sid = user.login(request, "testPassword") with db.begin(): - user2 = create(User, Username="test2", Email="test2@example.org", - Passwd="testPassword", AccountTypeID=USER_ID) + user2 = create( + User, + Username="test2", + Email="test2@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) post_data = { "U": "test", "E": "test@example.org", "TZ": "CET", - "passwd": "testPassword" + "passwd": "testPassword", } endpoint = f"/account/{user2.Username}/edit" with client as request: # Attempt to edit 'test2' while logged in as 'test'. - response = request.post(endpoint, cookies={"AURSID": sid}, - data=post_data, allow_redirects=False) + request.cookies = {"AURSID": sid} + response = request.post(endpoint, data=post_data) assert response.status_code == int(HTTPStatus.SEE_OTHER) expected = f"/account/{user2.Username}" @@ -1026,13 +1095,15 @@ def test_post_account_edit_ssh_pub_key(client: TestClient, user: User): "U": "test", "E": "test@example.org", "PK": make_ssh_pubkey(), - "passwd": "testPassword" + "passwd": "testPassword", } with client as request: - response = request.post("/account/test/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + request.cookies = {"AURSID": sid} + response = request.post( + "/account/test/edit", + data=post_data, + ) assert response.status_code == int(HTTPStatus.OK) @@ -1040,9 +1111,24 @@ def test_post_account_edit_ssh_pub_key(client: TestClient, user: User): post_data["PK"] = make_ssh_pubkey() with client as request: - response = request.post("/account/test/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + request.cookies = {"AURSID": sid} + response = request.post( + "/account/test/edit", + data=post_data, + ) + + assert response.status_code == int(HTTPStatus.OK) + + # Accidentally enter the same key twice + pk = make_ssh_pubkey() + post_data["PK"] = pk + "\n" + pk + + with client as request: + request.cookies = {"AURSID": sid} + response = request.post( + "/account/test/edit", + data=post_data, + ) assert response.status_code == int(HTTPStatus.OK) @@ -1055,13 +1141,15 @@ def test_post_account_edit_missing_ssh_pubkey(client: TestClient, user: User): "U": user.Username, "E": user.Email, "PK": make_ssh_pubkey(), - "passwd": "testPassword" + "passwd": "testPassword", } with client as request: - response = request.post("/account/test/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + request.cookies = {"AURSID": sid} + response = request.post( + "/account/test/edit", + data=post_data, + ) assert response.status_code == int(HTTPStatus.OK) @@ -1069,13 +1157,15 @@ def test_post_account_edit_missing_ssh_pubkey(client: TestClient, user: User): "U": user.Username, "E": user.Email, "PK": str(), # Pass an empty string now to walk the delete path. - "passwd": "testPassword" + "passwd": "testPassword", } with client as request: - response = request.post("/account/test/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + request.cookies = {"AURSID": sid} + response = request.post( + "/account/test/edit", + data=post_data, + ) assert response.status_code == int(HTTPStatus.OK) @@ -1087,12 +1177,12 @@ def test_post_account_edit_invalid_ssh_pubkey(client: TestClient, user: User): "U": "test", "E": "test@example.org", "PK": pubkey, - "passwd": "testPassword" + "passwd": "testPassword", } cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - response = request.post("/account/test/edit", data=data, - cookies=cookies, allow_redirects=False) + request.cookies = cookies + response = request.post("/account/test/edit", data=data) assert response.status_code == int(HTTPStatus.BAD_REQUEST) @@ -1106,13 +1196,15 @@ def test_post_account_edit_password(client: TestClient, user: User): "E": "test@example.org", "P": "newPassword", "C": "newPassword", - "passwd": "testPassword" + "passwd": "testPassword", } with client as request: - response = request.post("/account/test/edit", cookies={ - "AURSID": sid - }, data=post_data, allow_redirects=False) + request.cookies = {"AURSID": sid} + response = request.post( + "/account/test/edit", + data=post_data, + ) assert response.status_code == int(HTTPStatus.OK) @@ -1124,18 +1216,20 @@ def test_post_account_edit_self_type_as_user(client: TestClient, user: User): endpoint = f"/account/{user.Username}/edit" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) assert "id_type" not in resp.text data = { "U": user.Username, "E": user.Email, - "T": TRUSTED_USER_ID, - "passwd": "testPassword" + "T": PACKAGE_MAINTAINER_ID, + "passwd": "testPassword", } with client as request: - resp = request.post(endpoint, data=data, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) @@ -1151,90 +1245,150 @@ def test_post_account_edit_other_user_as_user(client: TestClient, user: User): endpoint = f"/account/{user2.Username}/edit" with client as request: - resp = request.get(endpoint, cookies=cookies, - allow_redirects=False) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/account/{user2.Username}" -def test_post_account_edit_self_type_as_tu(client: TestClient, tu_user: User): - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} - endpoint = f"/account/{tu_user.Username}/edit" +def test_post_account_edit_self_type_as_pm(client: TestClient, pm_user: User): + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} + endpoint = f"/account/{pm_user.Username}/edit" # We cannot see the Account Type field on our own edit page. with client as request: - resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) assert "id_type" in resp.text # We cannot modify our own account type. data = { - "U": tu_user.Username, - "E": tu_user.Email, + "U": pm_user.Username, + "E": pm_user.Email, "T": USER_ID, - "passwd": "testPassword" + "passwd": "testPassword", } with client as request: - resp = request.post(endpoint, data=data, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.OK) - assert tu_user.AccountTypeID == USER_ID + assert pm_user.AccountTypeID == USER_ID -def test_post_account_edit_other_user_type_as_tu( - client: TestClient, tu_user: User, caplog: pytest.LogCaptureFixture): +def test_post_account_edit_other_user_type_as_pm( + client: TestClient, pm_user: User, caplog: pytest.LogCaptureFixture +): caplog.set_level(DEBUG) with db.begin(): user2 = create_user("test2") - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} endpoint = f"/account/{user2.Username}/edit" - # As a TU, we can see the Account Type field for other users. + # As a PM, we can see the Account Type field for other users. with client as request: - resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) assert "id_type" in resp.text - # As a TU, we can modify other user's account types. + # As a PM, we can modify other user's account types. data = { "U": user2.Username, "E": user2.Email, - "T": TRUSTED_USER_ID, - "passwd": "testPassword" + "T": PACKAGE_MAINTAINER_ID, + "passwd": "testPassword", } + with client as request: - resp = request.post(endpoint, data=data, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.OK) # Let's make sure the DB got updated properly. - assert user2.AccountTypeID == TRUSTED_USER_ID + assert user2.AccountTypeID == PACKAGE_MAINTAINER_ID # and also that this got logged out at DEBUG level. - expected = (f"Trusted User '{tu_user.Username}' has " - f"modified '{user2.Username}' account's type to" - f" {TRUSTED_USER}.") + expected = ( + f"Package Maintainer '{pm_user.Username}' has " + f"modified '{user2.Username}' account's type to" + f" {PACKAGE_MAINTAINER}." + ) assert expected in caplog.text -def test_post_account_edit_other_user_type_as_tu_invalid_type( - client: TestClient, tu_user: User, caplog: pytest.LogCaptureFixture): +def test_post_account_edit_other_user_suspend_as_pm(client: TestClient, pm_user: User): + with db.begin(): + user = create_user("test3") + # Create a session for user + sid = user.login(Request(), "testPassword") + assert sid is not None + + # `user` needs its own TestClient, to keep its AURSID cookies + # apart from `pm_user`s during our testing. + user_client = TestClient(app=app) + user_client.headers.update(TEST_REFERER) + user_client.follow_redirects = False + + # Test that `user` can view their account edit page while logged in. + user_cookies = {"AURSID": sid} + with client as request: + endpoint = f"/account/{user.Username}/edit" + request.cookies = user_cookies + resp = request.get(endpoint) + assert resp.status_code == HTTPStatus.OK + + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} + assert cookies is not None # This is useless, we create the dict here ^ + # As a PM, we can see the Account for other users. + with client as request: + request.cookies = cookies + resp = request.get(endpoint) + assert resp.status_code == int(HTTPStatus.OK) + # As a PM, we can modify other user's account types. + data = { + "U": user.Username, + "E": user.Email, + "S": True, + "passwd": "testPassword", + } + with client as request: + request.cookies = cookies + resp = request.post(endpoint, data=data) + assert resp.status_code == int(HTTPStatus.OK) + + # Test that `user` no longer has a session. + with user_client as request: + request.cookies = user_cookies + resp = request.get(endpoint) + assert resp.status_code == HTTPStatus.SEE_OTHER + + # Since user is now suspended, they should not be able to login. + data = {"user": user.Username, "passwd": "testPassword", "next": "/"} + with user_client as request: + resp = request.post("/login", data=data) + assert resp.status_code == HTTPStatus.OK + errors = get_errors(resp.text) + assert errors[0].text.strip() == "Account Suspended" + + +def test_post_account_edit_other_user_type_as_pm_invalid_type( + client: TestClient, pm_user: User, caplog: pytest.LogCaptureFixture +): with db.begin(): user2 = create_user("test2") - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} endpoint = f"/account/{user2.Username}/edit" - # As a TU, we can modify other user's account types. - data = { - "U": user2.Username, - "E": user2.Email, - "T": 0, - "passwd": "testPassword" - } + # As a PM, we can modify other user's account types. + data = {"U": user2.Username, "E": user2.Email, "T": 0, "passwd": "testPassword"} with client as request: - resp = request.post(endpoint, data=data, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) @@ -1247,8 +1401,8 @@ def test_get_account(client: TestClient, user: User): sid = user.login(request, "testPassword") with client as request: - response = request.get("/account/test", cookies={"AURSID": sid}, - allow_redirects=False) + request.cookies = {"AURSID": sid} + response = request.get("/account/test") assert response.status_code == int(HTTPStatus.OK) @@ -1258,29 +1412,30 @@ def test_get_account_not_found(client: TestClient, user: User): sid = user.login(request, "testPassword") with client as request: - response = request.get("/account/not_found", cookies={"AURSID": sid}, - allow_redirects=False) + request.cookies = {"AURSID": sid} + response = request.get("/account/not_found") assert response.status_code == int(HTTPStatus.NOT_FOUND) def test_get_account_unauthenticated(client: TestClient, user: User): with client as request: - response = request.get("/account/test", allow_redirects=False) + response = request.get("/account/test") assert response.status_code == int(HTTPStatus.UNAUTHORIZED) content = response.content.decode() assert "You must log in to view user information." in content -def test_get_accounts(client: TestClient, user: User, tu_user: User): - """ Test that we can GET request /accounts and receive - a form which can be used to POST /accounts. """ +def test_get_accounts(client: TestClient, user: User, pm_user: User): + """Test that we can GET request /accounts and receive + a form which can be used to POST /accounts.""" sid = user.login(Request(), "testPassword") cookies = {"AURSID": sid} with client as request: - response = request.get("/accounts", cookies=cookies) + request.cookies = cookies + response = request.get("/accounts") assert response.status_code == int(HTTPStatus.OK) parser = lxml.etree.HTMLParser() @@ -1296,8 +1451,8 @@ def test_get_accounts(client: TestClient, user: User, tu_user: User): assert form.attrib.get("action") == "/accounts" def field(element): - """ Return the given element string as a valid - selector in the form. """ + """Return the given element string as a valid + selector in the form.""" return f"./fieldset/p/{element}" username = form.xpath(field('input[@id="id_username"]')) @@ -1332,7 +1487,7 @@ def get_rows(html): return root.xpath('//table[contains(@class, "users")]/tbody/tr') -def test_post_accounts(client: TestClient, user: User, tu_user: User): +def test_post_accounts(client: TestClient, user: User, pm_user: User): # Set a PGPKey. with db.begin(): user.PGPKey = "5F18B20346188419750745D7335F2CB41F253D30" @@ -1348,7 +1503,8 @@ def test_post_accounts(client: TestClient, user: User, tu_user: User): cookies = {"AURSID": sid} with client as request: - response = request.post("/accounts", cookies=cookies) + request.cookies = cookies + response = request.post("/accounts") assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1360,8 +1516,7 @@ def test_post_accounts(client: TestClient, user: User, tu_user: User): columns = rows[i].xpath("./td") assert len(columns) == 7 - username, atype, suspended, real_name, \ - irc_nick, pgp_key, edit = columns + username, atype, suspended, real_name, irc_nick, pgp_key, edit = columns username = next(iter(username.xpath("./a"))) assert username.text.strip() == _user.Username @@ -1379,18 +1534,20 @@ def test_post_accounts(client: TestClient, user: User, tu_user: User): else: assert not edit - logger.debug('Checked user row {"id": %s, "username": "%s"}.' - % (_user.ID, _user.Username)) + logger.debug( + 'Checked user row {"id": %s, "username": "%s"}.' + % (_user.ID, _user.Username) + ) -def test_post_accounts_username(client: TestClient, user: User, tu_user: User): +def test_post_accounts_username(client: TestClient, user: User, pm_user: User): # Test the U parameter path. sid = user.login(Request(), "testPassword") cookies = {"AURSID": sid} with client as request: - response = request.post("/accounts", cookies=cookies, - data={"U": user.Username}) + request.cookies = cookies + response = request.post("/accounts", data={"U": user.Username}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1403,34 +1560,35 @@ def test_post_accounts_username(client: TestClient, user: User, tu_user: User): assert username.text.strip() == user.Username -def test_post_accounts_account_type(client: TestClient, user: User, - tu_user: User): +def test_post_accounts_account_type(client: TestClient, user: User, pm_user: User): # Check the different account type options. sid = user.login(Request(), "testPassword") cookies = {"AURSID": sid} # Make a user with the "User" role here so we can # test the `u` parameter. - account_type = query(AccountType, - AccountType.AccountType == "User").first() + account_type = query(AccountType, AccountType.AccountType == "User").first() with db.begin(): - create(User, Username="test_2", - Email="test_2@example.org", - RealName="Test User 2", - Passwd="testPassword", - AccountType=account_type) + create( + User, + Username="test_2", + Email="test_2@example.org", + RealName="Test User 2", + Passwd="testPassword", + AccountType=account_type, + ) # Expect no entries; we marked our only user as a User type. with client as request: - response = request.post("/accounts", cookies=cookies, - data={"T": "t"}) + request.cookies = cookies + response = request.post("/accounts", data={"T": "t"}) assert response.status_code == int(HTTPStatus.OK) assert len(get_rows(response.text)) == 0 # So, let's also ensure that specifying "u" returns our user. with client as request: - response = request.post("/accounts", cookies=cookies, - data={"T": "u"}) + request.cookies = cookies + response = request.post("/accounts", data={"T": "u"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1441,15 +1599,15 @@ def test_post_accounts_account_type(client: TestClient, user: User, assert type.text.strip() == "User" - # Set our only user to a Trusted User. + # Set our only user to a Package Maintainer. with db.begin(): - user.AccountType = query(AccountType).filter( - AccountType.ID == TRUSTED_USER_ID - ).first() + user.AccountType = ( + query(AccountType).filter(AccountType.ID == PACKAGE_MAINTAINER_ID).first() + ) with client as request: - response = request.post("/accounts", cookies=cookies, - data={"T": "t"}) + request.cookies = cookies + response = request.post("/accounts", data={"T": "t"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1458,16 +1616,16 @@ def test_post_accounts_account_type(client: TestClient, user: User, row = next(iter(rows)) username, type, status, realname, irc, pgp_key, edit = row - assert type.text.strip() == "Trusted User" + assert type.text.strip() == "Package Maintainer" with db.begin(): - user.AccountType = query(AccountType).filter( - AccountType.ID == DEVELOPER_ID - ).first() + user.AccountType = ( + query(AccountType).filter(AccountType.ID == DEVELOPER_ID).first() + ) with client as request: - response = request.post("/accounts", cookies=cookies, - data={"T": "d"}) + request.cookies = cookies + response = request.post("/accounts", data={"T": "d"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1479,13 +1637,15 @@ def test_post_accounts_account_type(client: TestClient, user: User, assert type.text.strip() == "Developer" with db.begin(): - user.AccountType = query(AccountType).filter( - AccountType.ID == TRUSTED_USER_AND_DEV_ID - ).first() + user.AccountType = ( + query(AccountType) + .filter(AccountType.ID == PACKAGE_MAINTAINER_AND_DEV_ID) + .first() + ) with client as request: - response = request.post("/accounts", cookies=cookies, - data={"T": "td"}) + request.cookies = cookies + response = request.post("/accounts", data={"T": "td"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1494,16 +1654,17 @@ def test_post_accounts_account_type(client: TestClient, user: User, row = next(iter(rows)) username, type, status, realname, irc, pgp_key, edit = row - assert type.text.strip() == "Trusted User & Developer" + assert type.text.strip() == "Package Maintainer & Developer" -def test_post_accounts_status(client: TestClient, user: User, tu_user: User): +def test_post_accounts_status(client: TestClient, user: User, pm_user: User): # Test the functionality of Suspended. sid = user.login(Request(), "testPassword") cookies = {"AURSID": sid} with client as request: - response = request.post("/accounts", cookies=cookies) + request.cookies = cookies + response = request.post("/accounts") assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1517,8 +1678,8 @@ def test_post_accounts_status(client: TestClient, user: User, tu_user: User): user.Suspended = True with client as request: - response = request.post("/accounts", cookies=cookies, - data={"S": True}) + request.cookies = cookies + response = request.post("/accounts", data={"S": True}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1529,49 +1690,49 @@ def test_post_accounts_status(client: TestClient, user: User, tu_user: User): assert status.text.strip() == "Suspended" -def test_post_accounts_email(client: TestClient, user: User, tu_user: User): +def test_post_accounts_email(client: TestClient, user: User, pm_user: User): sid = user.login(Request(), "testPassword") cookies = {"AURSID": sid} # Search via email. with client as request: - response = request.post("/accounts", cookies=cookies, - data={"E": user.Email}) + request.cookies = cookies + response = request.post("/accounts", data={"E": user.Email}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) assert len(rows) == 1 -def test_post_accounts_realname(client: TestClient, user: User, tu_user: User): +def test_post_accounts_realname(client: TestClient, user: User, pm_user: User): # Test the R parameter path. sid = user.login(Request(), "testPassword") cookies = {"AURSID": sid} with client as request: - response = request.post("/accounts", cookies=cookies, - data={"R": user.RealName}) + request.cookies = cookies + response = request.post("/accounts", data={"R": user.RealName}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) assert len(rows) == 1 -def test_post_accounts_irc(client: TestClient, user: User, tu_user: User): +def test_post_accounts_irc(client: TestClient, user: User, pm_user: User): # Test the I parameter path. sid = user.login(Request(), "testPassword") cookies = {"AURSID": sid} with client as request: - response = request.post("/accounts", cookies=cookies, - data={"I": user.IRCNick}) + request.cookies = cookies + response = request.post("/accounts", data={"I": user.IRCNick}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) assert len(rows) == 1 -def test_post_accounts_sortby(client: TestClient, user: User, tu_user: User): +def test_post_accounts_sortby(client: TestClient, user: User, pm_user: User): # Create a second user so we can compare sorts. with db.begin(): user_ = create_user("test2") @@ -1582,29 +1743,29 @@ def test_post_accounts_sortby(client: TestClient, user: User, tu_user: User): # Show that "u" is the default search order, by username. with client as request: - response = request.post("/accounts", cookies=cookies) + request.cookies = cookies + response = request.post("/accounts") assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) assert len(rows) == 2 first_rows = rows with client as request: - response = request.post("/accounts", cookies=cookies, - data={"SB": "u"}) + request.cookies = cookies + response = request.post("/accounts", data={"SB": "u"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) assert len(rows) == 2 def compare_text_values(column, lhs, rhs): - return [row[column].text for row in lhs] \ - == [row[column].text for row in rhs] + return [row[column].text for row in lhs] == [row[column].text for row in rhs] # Test the username rows are ordered the same. assert compare_text_values(0, first_rows, rows) is True with client as request: - response = request.post("/accounts", cookies=cookies, - data={"SB": "i"}) + request.cookies = cookies + response = request.post("/accounts", data={"SB": "i"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) assert len(rows) == 2 @@ -1614,8 +1775,8 @@ def test_post_accounts_sortby(client: TestClient, user: User, tu_user: User): # Sort by "i" -> RealName. with client as request: - response = request.post("/accounts", cookies=cookies, - data={"SB": "r"}) + request.cookies = cookies + response = request.post("/accounts", data={"SB": "r"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) assert len(rows) == 2 @@ -1624,13 +1785,16 @@ def test_post_accounts_sortby(client: TestClient, user: User, tu_user: User): assert compare_text_values(4, first_rows, reversed(rows)) is True with db.begin(): - user.AccountType = query(AccountType).filter( - AccountType.ID == TRUSTED_USER_AND_DEV_ID - ).first() + user.AccountType = ( + query(AccountType) + .filter(AccountType.ID == PACKAGE_MAINTAINER_AND_DEV_ID) + .first() + ) # Fetch first_rows again with our new AccountType ordering. with client as request: - response = request.post("/accounts", cookies=cookies) + request.cookies = cookies + response = request.post("/accounts") assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) assert len(rows) == 2 @@ -1638,8 +1802,8 @@ def test_post_accounts_sortby(client: TestClient, user: User, tu_user: User): # Sort by "t" -> AccountType. with client as request: - response = request.post("/accounts", cookies=cookies, - data={"SB": "t"}) + request.cookies = cookies + response = request.post("/accounts", data={"SB": "t"}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) assert len(rows) == 2 @@ -1648,7 +1812,7 @@ def test_post_accounts_sortby(client: TestClient, user: User, tu_user: User): assert compare_text_values(1, first_rows, reversed(rows)) -def test_post_accounts_pgp_key(client: TestClient, user: User, tu_user: User): +def test_post_accounts_pgp_key(client: TestClient, user: User, pm_user: User): with db.begin(): user.PGPKey = "5F18B20346188419750745D7335F2CB41F253D30" @@ -1657,33 +1821,36 @@ def test_post_accounts_pgp_key(client: TestClient, user: User, tu_user: User): # Search via PGPKey. with client as request: - response = request.post("/accounts", cookies=cookies, - data={"K": user.PGPKey}) + request.cookies = cookies + response = request.post("/accounts", data={"K": user.PGPKey}) assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) assert len(rows) == 1 -def test_post_accounts_paged(client: TestClient, user: User, tu_user: User): +def test_post_accounts_paged(client: TestClient, user: User, pm_user: User): # Create 150 users. users = [user] - account_type = query(AccountType, - AccountType.AccountType == "User").first() + account_type = query(AccountType, AccountType.AccountType == "User").first() with db.begin(): for i in range(150): - _user = create(User, Username=f"test_#{i}", - Email=f"test_#{i}@example.org", - RealName=f"Test User #{i}", - Passwd="testPassword", - AccountType=account_type) + _user = create( + User, + Username=f"test_#{i}", + Email=f"test_#{i}@example.org", + RealName=f"Test User #{i}", + Passwd="testPassword", + AccountType=account_type, + ) users.append(_user) sid = user.login(Request(), "testPassword") cookies = {"AURSID": sid} with client as request: - response = request.post("/accounts", cookies=cookies) + request.cookies = cookies + response = request.post("/accounts") assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1709,8 +1876,8 @@ def test_post_accounts_paged(client: TestClient, user: User, tu_user: User): assert "disabled" not in page_next.attrib with client as request: - response = request.post("/accounts", cookies=cookies, - data={"O": 50}) # +50 offset. + request.cookies = cookies + response = request.post("/accounts", data={"O": 50}) # +50 offset. assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1724,8 +1891,8 @@ def test_post_accounts_paged(client: TestClient, user: User, tu_user: User): assert username.text.strip() == _user.Username with client as request: - response = request.post("/accounts", cookies=cookies, - data={"O": 101}) # Last page. + request.cookies = cookies + response = request.post("/accounts", data={"O": 101}) # Last page. assert response.status_code == int(HTTPStatus.OK) rows = get_rows(response.text) @@ -1741,11 +1908,12 @@ def test_post_accounts_paged(client: TestClient, user: User, tu_user: User): def test_get_terms_of_service(client: TestClient, user: User): with db.begin(): - term = create(Term, Description="Test term.", - URL="http://localhost", Revision=1) + term = create( + Term, Description="Test term.", URL="http://localhost", Revision=1 + ) with client as request: - response = request.get("/tos", allow_redirects=False) + response = request.get("/tos") assert response.status_code == int(HTTPStatus.SEE_OTHER) request = Request() @@ -1755,29 +1923,40 @@ def test_get_terms_of_service(client: TestClient, user: User): # First of all, let's test that we get redirected to /tos # when attempting to browse authenticated without accepting terms. with client as request: - response = request.get("/", cookies=cookies, allow_redirects=False) + request.cookies = cookies + response = request.get("/") assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/tos" with client as request: - response = request.get("/tos", cookies=cookies, allow_redirects=False) + request.cookies = cookies + response = request.get("/tos") assert response.status_code == int(HTTPStatus.OK) with db.begin(): - accepted_term = create(AcceptedTerm, User=user, - Term=term, Revision=term.Revision) + accepted_term = create( + AcceptedTerm, User=user, Term=term, Revision=term.Revision + ) with client as request: - response = request.get("/tos", cookies=cookies, allow_redirects=False) + request.cookies = cookies + response = request.get("/tos") # We accepted the term, there's nothing left to accept. assert response.status_code == int(HTTPStatus.SEE_OTHER) + # Make sure we don't get redirected to /tos when browsing "Home" + with client as request: + request.cookies = cookies + response = request.get("/") + assert response.status_code == int(HTTPStatus.OK) + # Bump the term's revision. with db.begin(): term.Revision = 2 with client as request: - response = request.get("/tos", cookies=cookies, allow_redirects=False) + request.cookies = cookies + response = request.get("/tos") # This time, we have a modified term Revision that hasn't # yet been agreed to via AcceptedTerm update. assert response.status_code == int(HTTPStatus.OK) @@ -1786,7 +1965,8 @@ def test_get_terms_of_service(client: TestClient, user: User): accepted_term.Revision = term.Revision with client as request: - response = request.get("/tos", cookies=cookies, allow_redirects=False) + request.cookies = cookies + response = request.get("/tos") # We updated the term revision, there's nothing left to accept. assert response.status_code == int(HTTPStatus.SEE_OTHER) @@ -1800,28 +1980,30 @@ def test_post_terms_of_service(client: TestClient, user: User): # Create a fresh Term. with db.begin(): - term = create(Term, Description="Test term.", - URL="http://localhost", Revision=1) + term = create( + Term, Description="Test term.", URL="http://localhost", Revision=1 + ) # Test that the term we just created is listed. with client as request: - response = request.get("/tos", cookies=cookies) + request.cookies = cookies + response = request.get("/tos") assert response.status_code == int(HTTPStatus.OK) # Make a POST request to /tos with the agree checkbox disabled (False). with client as request: - response = request.post("/tos", data={"accept": False}, - cookies=cookies) + request.cookies = cookies + response = request.post("/tos", data={"accept": False}) assert response.status_code == int(HTTPStatus.OK) # Make a POST request to /tos with the agree checkbox enabled (True). with client as request: - response = request.post("/tos", data=data, cookies=cookies) + request.cookies = cookies + response = request.post("/tos", data=data) assert response.status_code == int(HTTPStatus.SEE_OTHER) # Query the db for the record created by the post request. - accepted_term = query(AcceptedTerm, - AcceptedTerm.TermsID == term.ID).first() + accepted_term = query(AcceptedTerm, AcceptedTerm.TermsID == term.ID).first() assert accepted_term.User == user assert accepted_term.Term == term @@ -1831,12 +2013,14 @@ def test_post_terms_of_service(client: TestClient, user: User): # A GET request gives us the new revision to accept. with client as request: - response = request.get("/tos", cookies=cookies) + request.cookies = cookies + response = request.get("/tos") assert response.status_code == int(HTTPStatus.OK) # Let's POST again and agree to the new term revision. with client as request: - response = request.post("/tos", data=data, cookies=cookies) + request.cookies = cookies + response = request.post("/tos", data=data) assert response.status_code == int(HTTPStatus.SEE_OTHER) # Check that the records ended up matching. @@ -1844,7 +2028,8 @@ def test_post_terms_of_service(client: TestClient, user: User): # Now, see that GET redirects us to / with no terms left to accept. with client as request: - response = request.get("/tos", cookies=cookies, allow_redirects=False) + request.cookies = cookies + response = request.get("/tos") assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/" @@ -1852,13 +2037,154 @@ def test_post_terms_of_service(client: TestClient, user: User): def test_account_comments_not_found(client: TestClient, user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get("/account/non-existent/comments", cookies=cookies) + request.cookies = cookies + resp = request.get("/account/non-existent/comments") assert resp.status_code == int(HTTPStatus.NOT_FOUND) def test_accounts_unauthorized(client: TestClient, user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get("/accounts", cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.get("/accounts") assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == "/" + + +def test_account_delete_self_unauthorized(client: TestClient, pm_user: User): + with db.begin(): + user = create_user("some_user") + user2 = create_user("user2") + + cookies = {"AURSID": user.login(Request(), "testPassword")} + endpoint = f"/account/{user2.Username}/delete" + with client as request: + request.cookies = cookies + resp = request.get(endpoint) + assert resp.status_code == HTTPStatus.UNAUTHORIZED + + resp = request.post(endpoint) + assert resp.status_code == HTTPStatus.UNAUTHORIZED + + # But a PM does have access + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} + with TestClient(app=app) as request: + request.cookies = cookies + resp = request.get(endpoint) + assert resp.status_code == HTTPStatus.OK + + +def test_account_delete_self_not_found(client: TestClient, user: User): + cookies = {"AURSID": user.login(Request(), "testPassword")} + endpoint = "/account/non-existent-user/delete" + with client as request: + request.cookies = cookies + resp = request.get(endpoint) + assert resp.status_code == HTTPStatus.NOT_FOUND + + resp = request.post(endpoint) + assert resp.status_code == HTTPStatus.NOT_FOUND + + +def test_account_delete_self(client: TestClient, user: User): + username = user.Username + + # Confirm that we can view our own account deletion page + cookies = {"AURSID": user.login(Request(), "testPassword")} + endpoint = f"/account/{username}/delete" + with client as request: + request.cookies = cookies + resp = request.get(endpoint) + assert resp.status_code == HTTPStatus.OK + + # The checkbox must be checked + with client as request: + request.cookies = cookies + resp = request.post( + endpoint, + data={"passwd": "fakePassword", "confirm": False}, + ) + assert resp.status_code == HTTPStatus.BAD_REQUEST + errors = get_errors(resp.text) + assert ( + errors[0].text.strip() + == "The account has not been deleted, check the confirmation checkbox." + ) + + # The correct password must be supplied + with client as request: + request.cookies = cookies + resp = request.post( + endpoint, + data={"passwd": "fakePassword", "confirm": True}, + ) + assert resp.status_code == HTTPStatus.BAD_REQUEST + errors = get_errors(resp.text) + assert errors[0].text.strip() == "Invalid password." + + # Supply everything correctly and delete ourselves + with client as request: + request.cookies = cookies + resp = request.post( + endpoint, + data={"passwd": "testPassword", "confirm": True}, + ) + assert resp.status_code == HTTPStatus.SEE_OTHER + + # Check that our User record no longer exists in the database + record = db.query(User).filter(User.Username == username).first() + assert record is None + + +def test_account_delete_self_with_ssh_public_key(client: TestClient, user: User): + username = user.Username + + with db.begin(): + db.create( + SSHPubKey, User=user, Fingerprint="testFingerprint", PubKey="testPubKey" + ) + + # Confirm that we can view our own account deletion page + cookies = {"AURSID": user.login(Request(), "testPassword")} + endpoint = f"/account/{username}/delete" + with client as request: + request.cookies = cookies + resp = request.get(endpoint) + assert resp.status_code == HTTPStatus.OK + + # Supply everything correctly and delete ourselves + with client as request: + request.cookies = cookies + resp = request.post( + endpoint, + data={"passwd": "testPassword", "confirm": True}, + ) + assert resp.status_code == HTTPStatus.SEE_OTHER + + # Check that our User record no longer exists in the database + user_record = db.query(User).filter(User.Username == username).first() + assert user_record is None + sshpubkey_record = db.query(SSHPubKey).filter(SSHPubKey.User == user).first() + assert sshpubkey_record is None + + +def test_account_delete_as_pm(client: TestClient, pm_user: User): + with db.begin(): + user = create_user("user2") + + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} + username = user.Username + endpoint = f"/account/{username}/delete" + + # Delete the user + with client as request: + request.cookies = cookies + resp = request.post( + endpoint, + data={"passwd": "testPassword", "confirm": True}, + ) + assert resp.status_code == HTTPStatus.SEE_OTHER + + # Check that our User record no longer exists in the database + record = db.query(User).filter(User.Username == username).first() + assert record is None diff --git a/test/test_adduser.py b/test/test_adduser.py index c6210e74..13648d5e 100644 --- a/test/test_adduser.py +++ b/test/test_adduser.py @@ -1,19 +1,19 @@ -from typing import List from unittest import mock import pytest import aurweb.models.account_type as at - from aurweb import db from aurweb.models import User from aurweb.scripts import adduser from aurweb.testing.requests import Request -TEST_SSH_PUBKEY = ("ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAI" - "bmlzdHAyNTYAAABBBEURnkiY6JoLyqDE8Li1XuAW+LHmkmLDMW/GL5wY" - "7k4/A+Ta7bjA3MOKrF9j4EuUTvCuNXULxvpfSqheTFWZc+g= " - "kevr@volcano") +TEST_SSH_PUBKEY = ( + "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAI" + "bmlzdHAyNTYAAABBBEURnkiY6JoLyqDE8Li1XuAW+LHmkmLDMW/GL5wY" + "7k4/A+Ta7bjA3MOKrF9j4EuUTvCuNXULxvpfSqheTFWZc+g= " + "kevr@volcano" +) @pytest.fixture(autouse=True) @@ -21,7 +21,7 @@ def setup(db_test): return -def run_main(args: List[str] = []): +def run_main(args: list[str] = []): with mock.patch("sys.argv", ["aurweb-adduser"] + args): adduser.main() @@ -38,19 +38,37 @@ def test_adduser(): assert test.login(Request(), "abcd1234") -def test_adduser_tu(): - run_main([ - "-u", "test", "-e", "test@example.org", "-p", "abcd1234", - "-t", at.TRUSTED_USER - ]) +def test_adduser_pm(): + run_main( + [ + "-u", + "test", + "-e", + "test@example.org", + "-p", + "abcd1234", + "-t", + at.PACKAGE_MAINTAINER, + ] + ) test = db.query(User).filter(User.Username == "test").first() assert test is not None - assert test.AccountTypeID == at.TRUSTED_USER_ID + assert test.AccountTypeID == at.PACKAGE_MAINTAINER_ID def test_adduser_ssh_pk(): - run_main(["-u", "test", "-e", "test@example.org", "-p", "abcd1234", - "--ssh-pubkey", TEST_SSH_PUBKEY]) + run_main( + [ + "-u", + "test", + "-e", + "test@example.org", + "-p", + "abcd1234", + "--ssh-pubkey", + TEST_SSH_PUBKEY, + ] + ) test = db.query(User).filter(User.Username == "test").first() assert test is not None assert TEST_SSH_PUBKEY.startswith(test.ssh_pub_keys.first().PubKey) diff --git a/test/test_api_rate_limit.py b/test/test_api_rate_limit.py index 82805ecf..c67aa57d 100644 --- a/test/test_api_rate_limit.py +++ b/test/test_api_rate_limit.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -13,8 +12,7 @@ def setup(db_test): def test_api_rate_key_creation(): with db.begin(): - rate = db.create(ApiRateLimit, IP="127.0.0.1", Requests=10, - WindowStart=1) + rate = db.create(ApiRateLimit, IP="127.0.0.1", Requests=10, WindowStart=1) assert rate.IP == "127.0.0.1" assert rate.Requests == 10 assert rate.WindowStart == 1 diff --git a/test/test_asgi.py b/test/test_asgi.py index c693a3a9..840e6608 100644 --- a/test/test_asgi.py +++ b/test/test_asgi.py @@ -1,29 +1,26 @@ import http import os import re - from typing import Callable from unittest import mock import fastapi import pytest - from fastapi import HTTPException from fastapi.testclient import TestClient import aurweb.asgi +import aurweb.aur_redis import aurweb.config -import aurweb.redis - from aurweb.exceptions import handle_form_exceptions from aurweb.testing.requests import Request @pytest.fixture def setup(db_test, email_test): - aurweb.redis.redis_connection().flushall() + aurweb.aur_redis.redis_connection().flushall() yield - aurweb.redis.redis_connection().flushall() + aurweb.aur_redis.redis_connection().flushall() @pytest.fixture @@ -33,7 +30,9 @@ def mock_glab_request(monkeypatch): if side_effect: return side_effect # pragma: no cover return return_value + monkeypatch.setattr("requests.post", what_to_return) + return wrapped @@ -47,13 +46,14 @@ def mock_glab_config(project: str = "test/project", token: str = "test-token"): elif key == "error-token": return token return config_get(section, key) + return wrapper @pytest.mark.asyncio async def test_asgi_startup_session_secret_exception(monkeypatch): - """ Test that we get an IOError on app_startup when we cannot - connect to options.redis_address. """ + """Test that we get an IOError on app_startup when we cannot + connect to options.redis_address.""" redis_addr = aurweb.config.get("options", "redis_address") @@ -68,12 +68,19 @@ async def test_asgi_startup_session_secret_exception(monkeypatch): @pytest.mark.asyncio -async def test_asgi_startup_exception(monkeypatch): - with mock.patch.dict(os.environ, {"AUR_CONFIG": "conf/config.defaults"}): - aurweb.config.rehash() - with pytest.raises(Exception): - await aurweb.asgi.app_startup() - aurweb.config.rehash() +async def test_asgi_startup_exception(): + # save proper session secret + prev_secret = aurweb.asgi.session_secret + + # remove secret + aurweb.asgi.session_secret = None + + # startup should fail + with pytest.raises(Exception): + await aurweb.asgi.app_startup() + + # restore previous session secret after test + aurweb.asgi.session_secret = prev_secret @pytest.mark.asyncio @@ -110,8 +117,9 @@ async def test_asgi_app_disabled_metrics(caplog: pytest.LogCaptureFixture): with mock.patch.dict(os.environ, env): await aurweb.asgi.app_startup() - expected = ("$PROMETHEUS_MULTIPROC_DIR is not set, the /metrics " - "endpoint is disabled.") + expected = ( + "$PROMETHEUS_MULTIPROC_DIR is not set, the /metrics " "endpoint is disabled." + ) assert expected in caplog.text @@ -134,9 +142,12 @@ class FakeResponse: self.text = text -def test_internal_server_error_bad_glab(setup: None, use_traceback: None, - mock_glab_request: Callable, - caplog: pytest.LogCaptureFixture): +def test_internal_server_error_bad_glab( + setup: None, + use_traceback: None, + mock_glab_request: Callable, + caplog: pytest.LogCaptureFixture, +): @aurweb.asgi.app.get("/internal_server_error") async def internal_server_error(request: fastapi.Request): raise ValueError("test exception") @@ -154,9 +165,12 @@ def test_internal_server_error_bad_glab(setup: None, use_traceback: None, assert re.search(expr, caplog.text) -def test_internal_server_error_no_token(setup: None, use_traceback: None, - mock_glab_request: Callable, - caplog: pytest.LogCaptureFixture): +def test_internal_server_error_no_token( + setup: None, + use_traceback: None, + mock_glab_request: Callable, + caplog: pytest.LogCaptureFixture, +): @aurweb.asgi.app.get("/internal_server_error") async def internal_server_error(request: fastapi.Request): raise ValueError("test exception") @@ -175,9 +189,12 @@ def test_internal_server_error_no_token(setup: None, use_traceback: None, assert re.search(expr, caplog.text) -def test_internal_server_error(setup: None, use_traceback: None, - mock_glab_request: Callable, - caplog: pytest.LogCaptureFixture): +def test_internal_server_error( + setup: None, + use_traceback: None, + mock_glab_request: Callable, + caplog: pytest.LogCaptureFixture, +): @aurweb.asgi.app.get("/internal_server_error") async def internal_server_error(request: fastapi.Request): raise ValueError("test exception") @@ -203,9 +220,12 @@ def test_internal_server_error(setup: None, use_traceback: None, assert "FATAL" not in caplog.text -def test_internal_server_error_post(setup: None, use_traceback: None, - mock_glab_request: Callable, - caplog: pytest.LogCaptureFixture): +def test_internal_server_error_post( + setup: None, + use_traceback: None, + mock_glab_request: Callable, + caplog: pytest.LogCaptureFixture, +): @aurweb.asgi.app.post("/internal_server_error") @handle_form_exceptions async def internal_server_error(request: fastapi.Request): diff --git a/test/test_aurblup.py b/test/test_aurblup.py index 0b499d57..1489677d 100644 --- a/test/test_aurblup.py +++ b/test/test_aurblup.py @@ -1,5 +1,4 @@ import tempfile - from unittest import mock import py @@ -32,7 +31,7 @@ def setup(db_test, alpm_db: AlpmDatabase, tempdir: py.path.local) -> None: if key == "db-path": return alpm_db.local elif key == "server": - return f'file://{alpm_db.remote}' + return f"file://{alpm_db.remote}" elif key == "sync-dbs": return alpm_db.repo return value @@ -51,8 +50,7 @@ def test_aurblup(alpm_db: AlpmDatabase): # Test that the package got added to the database. for name in ("pkg", "pkg2"): - pkg = db.query(OfficialProvider).filter( - OfficialProvider.Name == name).first() + pkg = db.query(OfficialProvider).filter(OfficialProvider.Name == name).first() assert pkg is not None # Test that we can remove the package. @@ -62,11 +60,9 @@ def test_aurblup(alpm_db: AlpmDatabase): aurblup.main(True) # Expect that the database got updated accordingly. - pkg = db.query(OfficialProvider).filter( - OfficialProvider.Name == "pkg").first() + pkg = db.query(OfficialProvider).filter(OfficialProvider.Name == "pkg").first() assert pkg is None - pkg2 = db.query(OfficialProvider).filter( - OfficialProvider.Name == "pkg2").first() + pkg2 = db.query(OfficialProvider).filter(OfficialProvider.Name == "pkg2").first() assert pkg2 is not None @@ -78,14 +74,36 @@ def test_aurblup_cleanup(alpm_db: AlpmDatabase): # Now, let's insert an OfficialPackage that doesn't exist, # then exercise the old provider deletion path. with db.begin(): - db.create(OfficialProvider, Name="fake package", - Repo="test", Provides="package") + db.create( + OfficialProvider, Name="fake package", Repo="test", Provides="package" + ) # Run aurblup again. aurblup.main() # Expect that the fake package got deleted because it's # not in alpm_db anymore. - providers = db.query(OfficialProvider).filter( - OfficialProvider.Name == "fake package").all() + providers = ( + db.query(OfficialProvider).filter(OfficialProvider.Name == "fake package").all() + ) assert len(providers) == 0 + + +def test_aurblup_repo_change(alpm_db: AlpmDatabase): + # Add a package and sync up the database. + alpm_db.add("pkg", "1.0", "x86_64", provides=["pkg2", "pkg3"]) + aurblup.main() + + # We should find an entry with repo "test" + op = db.query(OfficialProvider).filter(OfficialProvider.Name == "pkg").first() + assert op.Repo == "test" + + # Modify the repo to something that does not exist. + op.Repo = "nonsense" + + # Run our script. + aurblup.main() + + # Repo should be set back to "test" + db.refresh(op) + assert op.Repo == "test" diff --git a/test/test_auth.py b/test/test_auth.py index b8221c19..f3502faf 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -1,11 +1,15 @@ import fastapi import pytest - from fastapi import HTTPException from sqlalchemy.exc import IntegrityError from aurweb import config, db, time -from aurweb.auth import AnonymousUser, BasicAuthBackend, _auth_required, account_type_required +from aurweb.auth import ( + AnonymousUser, + BasicAuthBackend, + _auth_required, + account_type_required, +) from aurweb.models.account_type import USER, USER_ID from aurweb.models.session import Session from aurweb.models.user import User @@ -20,9 +24,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.com", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.com", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @@ -55,8 +64,7 @@ async def test_auth_backend_invalid_user_id(): # Create a new session with a fake user id. now_ts = time.utcnow() with pytest.raises(IntegrityError): - Session(UsersID=666, SessionID="realSession", - LastUpdateTS=now_ts + 5) + Session(UsersID=666, SessionID="realSession", LastUpdateTS=now_ts + 5) @pytest.mark.asyncio @@ -65,8 +73,9 @@ async def test_basic_auth_backend(user: User, backend: BasicAuthBackend): # equal the real_user. now_ts = time.utcnow() with db.begin(): - db.create(Session, UsersID=user.ID, SessionID="realSession", - LastUpdateTS=now_ts + 5) + db.create( + Session, UsersID=user.ID, SessionID="realSession", LastUpdateTS=now_ts + 5 + ) request = Request() request.cookies["AURSID"] = "realSession" @@ -76,7 +85,7 @@ async def test_basic_auth_backend(user: User, backend: BasicAuthBackend): @pytest.mark.asyncio async def test_expired_session(backend: BasicAuthBackend, user: User): - """ Login, expire the session manually, then authenticate. """ + """Login, expire the session manually, then authenticate.""" # First, build a Request with a logged in user. request = Request() request.user = user @@ -115,8 +124,8 @@ async def test_auth_required_redirection_bad_referrer(): def test_account_type_required(): - """ This test merely asserts that a few different paths - do not raise exceptions. """ + """This test merely asserts that a few different paths + do not raise exceptions.""" # This one shouldn't raise. account_type_required({USER}) @@ -125,12 +134,12 @@ def test_account_type_required(): # But this one should! We have no "FAKE" key. with pytest.raises(KeyError): - account_type_required({'FAKE'}) + account_type_required({"FAKE"}) -def test_is_trusted_user(): +def test_is_package_maintainer(): user_ = AnonymousUser() - assert not user_.is_trusted_user() + assert not user_.is_package_maintainer() def test_is_developer(): diff --git a/test/test_auth_routes.py b/test/test_auth_routes.py index 8467adea..066457c4 100644 --- a/test/test_auth_routes.py +++ b/test/test_auth_routes.py @@ -1,14 +1,11 @@ import re - from http import HTTPStatus from unittest import mock import pytest - from fastapi.testclient import TestClient import aurweb.config - from aurweb import db, time from aurweb.asgi import app from aurweb.models.account_type import USER_ID @@ -36,45 +33,49 @@ def client() -> TestClient: # Necessary for forged login CSRF protection on the login route. Set here # instead of only on the necessary requests for convenience. client.headers.update(TEST_REFERER) + + # disable redirects for our tests + client.follow_redirects = False yield client @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username=TEST_USERNAME, Email=TEST_EMAIL, - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username=TEST_USERNAME, + Email=TEST_EMAIL, + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user def test_login_logout(client: TestClient, user: User): - post_data = { - "user": "test", - "passwd": "testPassword", - "next": "/" - } + post_data = {"user": "test", "passwd": "testPassword", "next": "/"} with client as request: # First, let's test get /login. response = request.get("/login") assert response.status_code == int(HTTPStatus.OK) - response = request.post("/login", data=post_data, - allow_redirects=False) + response = request.post("/login", data=post_data) assert response.status_code == int(HTTPStatus.SEE_OTHER) # Simulate following the redirect location from above's response. response = request.get(response.headers.get("location")) assert response.status_code == int(HTTPStatus.OK) - response = request.post("/logout", data=post_data, - allow_redirects=False) + response = request.post("/logout", data=post_data) assert response.status_code == int(HTTPStatus.SEE_OTHER) - response = request.post("/logout", data=post_data, cookies={ - "AURSID": response.cookies.get("AURSID") - }, allow_redirects=False) + request.cookies = {"AURSID": response.cookies.get("AURSID")} + response = request.post( + "/logout", + data=post_data, + ) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert "AURSID" not in response.cookies @@ -84,11 +85,7 @@ def test_login_suspended(client: TestClient, user: User): with db.begin(): user.Suspended = 1 - data = { - "user": user.Username, - "passwd": "testPassword", - "next": "/" - } + data = {"user": user.Username, "passwd": "testPassword", "next": "/"} with client as request: resp = request.post("/login", data=data) errors = get_errors(resp.text) @@ -96,35 +93,62 @@ def test_login_suspended(client: TestClient, user: User): def test_login_email(client: TestClient, user: user): - post_data = { - "user": user.Email, - "passwd": "testPassword", - "next": "/" - } + post_data = {"user": user.Email, "passwd": "testPassword", "next": "/"} with client as request: - resp = request.post("/login", data=post_data, - allow_redirects=False) + resp = request.post("/login", data=post_data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert "AURSID" in resp.cookies -def mock_getboolean(a, b): - if a == "options" and b == "disable_http_login": - return True - return bool(aurweb.config.get(a, b)) +def mock_getboolean(**overrided_configs): + mocked_config = { + tuple(config.split("__")): value for config, value in overrided_configs.items() + } + + def side_effect(*args): + return mocked_config.get(args, bool(aurweb.config.get(*args))) + + return side_effect -@mock.patch("aurweb.config.getboolean", side_effect=mock_getboolean) -def test_secure_login(getboolean: bool, client: TestClient, user: User): - """ In this test, we check to verify the course of action taken +@mock.patch( + "aurweb.config.getboolean", + side_effect=mock_getboolean(options__disable_http_login=False), +) +def test_insecure_login(getboolean: mock.Mock, client: TestClient, user: User): + post_data = {"user": user.Username, "passwd": "testPassword", "next": "/"} + + # Perform a login request with the data matching our user. + with client as request: + response = request.post("/login", data=post_data) + + # Make sure we got the expected status out of it. + assert response.status_code == int(HTTPStatus.SEE_OTHER) + + # Let's check what we got in terms of cookies for AURSID. + # Make sure that a secure cookie got passed to us. + cookie = next(c for c in response.cookies.jar if c.name == "AURSID") + assert cookie.secure is False + assert cookie.has_nonstandard_attr("HttpOnly") is False + assert cookie.has_nonstandard_attr("SameSite") is True + assert cookie.get_nonstandard_attr("SameSite") == "lax" + assert cookie.value is not None and len(cookie.value) > 0 + + +@mock.patch( + "aurweb.config.getboolean", + side_effect=mock_getboolean(options__disable_http_login=True), +) +def test_secure_login(getboolean: mock.Mock, client: TestClient, user: User): + """In this test, we check to verify the course of action taken by starlette when providing secure=True to a response cookie. This is achieved by mocking aurweb.config.getboolean to return True (or 1) when looking for `options.disable_http_login`. When we receive a response with `disable_http_login` enabled, we check the fields in cookies received for the secure and httponly fields, in addition to the rest of the fields given - on such a request. """ + on such a request.""" # Create a local TestClient here since we mocked configuration. # client = TestClient(app) @@ -134,27 +158,22 @@ def test_secure_login(getboolean: bool, client: TestClient, user: User): # client.headers.update(TEST_REFERER) # Data used for our upcoming http post request. - post_data = { - "user": user.Username, - "passwd": "testPassword", - "next": "/" - } + post_data = {"user": user.Username, "passwd": "testPassword", "next": "/"} # Perform a login request with the data matching our user. with client as request: - response = request.post("/login", data=post_data, - allow_redirects=False) + response = request.post("/login", data=post_data) # Make sure we got the expected status out of it. assert response.status_code == int(HTTPStatus.SEE_OTHER) # Let's check what we got in terms of cookies for AURSID. # Make sure that a secure cookie got passed to us. - cookie = next(c for c in response.cookies if c.name == "AURSID") + cookie = next(c for c in response.cookies.jar if c.name == "AURSID") assert cookie.secure is True assert cookie.has_nonstandard_attr("HttpOnly") is True assert cookie.has_nonstandard_attr("SameSite") is True - assert cookie.get_nonstandard_attr("SameSite") == "strict" + assert cookie.get_nonstandard_attr("SameSite") == "lax" assert cookie.value is not None and len(cookie.value) > 0 # Let's make sure we actually have a session relationship @@ -165,16 +184,11 @@ def test_secure_login(getboolean: bool, client: TestClient, user: User): def test_authenticated_login(client: TestClient, user: User): - post_data = { - "user": user.Username, - "passwd": "testPassword", - "next": "/" - } + post_data = {"user": user.Username, "passwd": "testPassword", "next": "/"} with client as request: # Try to login. - response = request.post("/login", data=post_data, - allow_redirects=False) + response = request.post("/login", data=post_data) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/" @@ -182,8 +196,9 @@ def test_authenticated_login(client: TestClient, user: User): # when requesting GET /login as an authenticated user. # Now, let's verify that we receive 403 Forbidden when we # try to get /login as an authenticated user. - response = request.get("/login", cookies=response.cookies, - allow_redirects=False) + request.cookies = response.cookies + response = request.get("/login") + assert response.status_code == int(HTTPStatus.OK) assert "Logged-in as: test" in response.text @@ -192,16 +207,13 @@ def test_unauthenticated_logout_unauthorized(client: TestClient): with client as request: # Alright, let's verify that attempting to /logout when not # authenticated returns 401 Unauthorized. - response = request.post("/logout", allow_redirects=False) + response = request.post("/logout") assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location").startswith("/login") def test_login_missing_username(client: TestClient): - post_data = { - "passwd": "testPassword", - "next": "/" - } + post_data = {"passwd": "testPassword", "next": "/"} with client as request: response = request.post("/login", data=post_data) @@ -218,17 +230,15 @@ def test_login_remember_me(client: TestClient, user: User): "user": "test", "passwd": "testPassword", "next": "/", - "remember_me": True + "remember_me": True, } with client as request: - response = request.post("/login", data=post_data, - allow_redirects=False) + response = request.post("/login", data=post_data) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert "AURSID" in response.cookies - cookie_timeout = aurweb.config.getint( - "options", "persistent_cookie_timeout") + cookie_timeout = aurweb.config.getint("options", "persistent_cookie_timeout") now_ts = time.utcnow() session = db.query(Session).filter(Session.UsersID == user.ID).first() @@ -242,7 +252,7 @@ def test_login_incorrect_password_remember_me(client: TestClient, user: User): "user": "test", "passwd": "badPassword", "next": "/", - "remember_me": "on" + "remember_me": "on", } with client as request: @@ -257,10 +267,7 @@ def test_login_incorrect_password_remember_me(client: TestClient, user: User): def test_login_missing_password(client: TestClient): - post_data = { - "user": "test", - "next": "/" - } + post_data = {"user": "test", "next": "/"} with client as request: response = request.post("/login", data=post_data) @@ -272,11 +279,7 @@ def test_login_missing_password(client: TestClient): def test_login_incorrect_password(client: TestClient): - post_data = { - "user": "test", - "passwd": "badPassword", - "next": "/" - } + post_data = {"user": "test", "passwd": "badPassword", "next": "/"} with client as request: response = request.post("/login", data=post_data) @@ -312,8 +315,9 @@ def test_login_bad_referer(client: TestClient): assert "AURSID" not in response.cookies -def test_generate_unique_sid_exhausted(client: TestClient, user: User, - caplog: pytest.LogCaptureFixture): +def test_generate_unique_sid_exhausted( + client: TestClient, user: User, caplog: pytest.LogCaptureFixture +): """ In this test, we mock up generate_unique_sid() to infinitely return the same SessionID given to `user`. Within that mocking, we try @@ -326,13 +330,17 @@ def test_generate_unique_sid_exhausted(client: TestClient, user: User, now = time.utcnow() with db.begin(): # Create a second user; we'll login with this one. - user2 = db.create(User, Username="test2", Email="test2@example.org", - ResetKey="testReset", Passwd="testPassword", - AccountTypeID=USER_ID) + user2 = db.create( + User, + Username="test2", + Email="test2@example.org", + ResetKey="testReset", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) # Create a session with ID == "testSession" for `user`. - db.create(Session, User=user, SessionID="testSession", - LastUpdateTS=now) + db.create(Session, User=user, SessionID="testSession", LastUpdateTS=now) # Mock out generate_unique_sid; always return "testSession" which # causes us to eventually error out and raise an internal error. @@ -350,7 +358,8 @@ def test_generate_unique_sid_exhausted(client: TestClient, user: User, with mock.patch(generate_unique_sid_, mock_generate_sid): with client as request: # Set cookies = {} to remove any previous login kept by TestClient. - response = request.post("/login", data=post_data, cookies={}) + request.cookies = {} + response = request.post("/login", data=post_data) assert response.status_code == int(HTTPStatus.INTERNAL_SERVER_ERROR) assert "500 - Internal Server Error" in response.text diff --git a/test/test_ban.py b/test/test_ban.py index ff49f7e2..f795c27c 100644 --- a/test/test_ban.py +++ b/test/test_ban.py @@ -1,9 +1,7 @@ import warnings - -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import pytest - from sqlalchemy import exc as sa_exc from aurweb import db @@ -19,7 +17,7 @@ def setup(db_test): @pytest.fixture def ban() -> Ban: - ts = datetime.utcnow() + timedelta(seconds=30) + ts = datetime.now(UTC) + timedelta(seconds=30) with db.begin(): ban = create(Ban, IPAddress="127.0.0.1", BanTS=ts) yield ban @@ -32,7 +30,7 @@ def test_ban(ban: Ban): def test_invalid_ban(): with pytest.raises(sa_exc.IntegrityError): - bad_ban = Ban(BanTS=datetime.utcnow()) + bad_ban = Ban(BanTS=datetime.now(UTC)) # We're adding a ban with no primary key; this causes an # SQLAlchemy warnings when committing to the DB. diff --git a/test/test_cache.py b/test/test_cache.py index b49ee386..a599ab32 100644 --- a/test/test_cache.py +++ b/test/test_cache.py @@ -1,6 +1,8 @@ +from unittest import mock + import pytest -from aurweb import cache, db +from aurweb import cache, config, db from aurweb.models.account_type import USER_ID from aurweb.models.user import User @@ -10,62 +12,83 @@ def setup(db_test): return -class StubRedis: - """ A class which acts as a RedisConnection without using Redis. """ - - cache = dict() - expires = dict() - - def get(self, key, *args): - if "key" not in self.cache: - self.cache[key] = None - return self.cache[key] - - def set(self, key, *args): - self.cache[key] = list(args)[0] - - def expire(self, key, *args): - self.expires[key] = list(args)[0] - - async def execute(self, command, key, *args): - f = getattr(self, command) - return f(key, *args) - - @pytest.fixture -def redis(): - yield StubRedis() +def user() -> User: + with db.begin(): + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) + yield user -@pytest.mark.asyncio -async def test_db_count_cache(redis): - db.create(User, Username="user1", - Email="user1@example.org", - Passwd="testPassword", - AccountTypeID=USER_ID) +@pytest.fixture(autouse=True) +def clear_fakeredis_cache(): + cache._redis.flushall() + +def test_db_count_cache(user): query = db.query(User) - # Now, perform several checks that db_count_cache matches query.count(). - # We have no cached value yet. - assert await cache.db_count_cache(redis, "key1", query) == query.count() + assert cache._redis.get("key1") is None + + # Add to cache + assert cache.db_count_cache("key1", query) == query.count() # It's cached now. - assert await cache.db_count_cache(redis, "key1", query) == query.count() + assert cache._redis.get("key1") is not None - -@pytest.mark.asyncio -async def test_db_count_cache_expires(redis): - db.create(User, Username="user1", - Email="user1@example.org", - Passwd="testPassword", - AccountTypeID=USER_ID) - - query = db.query(User) + # It does not expire + assert cache._redis.ttl("key1") == -1 # Cache a query with an expire. - value = await cache.db_count_cache(redis, "key1", query, 100) + value = cache.db_count_cache("key2", query, 100) assert value == query.count() - assert redis.expires["key1"] == 100 + assert cache._redis.ttl("key2") == 100 + + +def test_db_query_cache(user): + query = db.query(User) + + # We have no cached value yet. + assert cache._redis.get("key1") is None + + # Add to cache + cache.db_query_cache("key1", query) + + # It's cached now. + assert cache._redis.get("key1") is not None + + # Modify our user and make sure we got a cached value + user.Username = "changed" + cached = cache.db_query_cache("key1", query) + assert cached[0].Username != query.all()[0].Username + + # It does not expire + assert cache._redis.ttl("key1") == -1 + + # Cache a query with an expire. + value = cache.db_query_cache("key2", query, 100) + assert len(value) == query.count() + assert value[0].Username == query.all()[0].Username + + assert cache._redis.ttl("key2") == 100 + + # Test "max_search_entries" options + def mock_max_search_entries(section: str, key: str, fallback: int) -> str: + if section == "cache" and key == "max_search_entries": + return 1 + return config.getint(section, key) + + with mock.patch("aurweb.config.getint", side_effect=mock_max_search_entries): + # Try to add another entry (we already have 2) + cache.db_query_cache("key3", query) + + # Make sure it was not added because it exceeds our max. + assert cache._redis.get("key3") is None diff --git a/test/test_captcha.py b/test/test_captcha.py index e5f8c71a..fa6fcbcc 100644 --- a/test/test_captcha.py +++ b/test/test_captcha.py @@ -11,14 +11,14 @@ def setup(db_test): def test_captcha_salts(): - """ Make sure we can get some captcha salts. """ + """Make sure we can get some captcha salts.""" salts = captcha.get_captcha_salts() assert len(salts) == 6 def test_captcha_token(): - """ Make sure getting a captcha salt's token matches up against - the first three digits of the md5 hash of the salt. """ + """Make sure getting a captcha salt's token matches up against + the first three digits of the md5 hash of the salt.""" salts = captcha.get_captcha_salts() salt = salts[0] @@ -29,9 +29,9 @@ def test_captcha_token(): def test_captcha_challenge_answer(): - """ Make sure that executing the captcha challenge via shell + """Make sure that executing the captcha challenge via shell produces the correct result by comparing it against a straight - up token conversion. """ + up token conversion.""" salts = captcha.get_captcha_salts() salt = salts[0] @@ -44,7 +44,7 @@ def test_captcha_challenge_answer(): def test_captcha_salt_filter(): - """ Make sure captcha_salt_filter returns the first salt from + """Make sure captcha_salt_filter returns the first salt from get_captcha_salts(). Example usage: @@ -55,7 +55,7 @@ def test_captcha_salt_filter(): def test_captcha_cmdline_filter(): - """ Make sure that the captcha_cmdline filter gives us the + """Make sure that the captcha_cmdline filter gives us the same challenge that get_captcha_challenge does. Example usage: diff --git a/test/test_config.py b/test/test_config.py index f451d8b3..c7a3610e 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -2,7 +2,6 @@ import configparser import io import os import re - from unittest import mock import py @@ -35,6 +34,7 @@ def mock_config_get(): if option == "salt_rounds": return "666" return config_get(section, option) + return _mock_config_get @@ -59,7 +59,7 @@ def test_config_main_get_unknown_section(get: str): main() # With an invalid section, we should get a usage error. - expected = r'^error: no section found$' + expected = r"^error: no section found$" assert re.match(expected, stderr.getvalue().strip()) @@ -140,8 +140,7 @@ def test_config_main_set_immutable(): args = ["aurweb-config", "set", "options", "salt_rounds", "666"] with mock.patch.dict(os.environ, {"AUR_CONFIG_IMMUTABLE": "1"}): with mock.patch("sys.argv", args): - with mock.patch("aurweb.config.set_option", - side_effect=mock_set_option): + with mock.patch("aurweb.config.set_option", side_effect=mock_set_option): main() expected = None @@ -170,8 +169,7 @@ def test_config_main_set_unknown_section(save: None): args = ["aurweb-config", "set", "options", "salt_rounds", "666"] with mock.patch("sys.argv", args): with mock.patch("sys.stderr", stderr): - with mock.patch("aurweb.config.set_option", - side_effect=mock_set_option): + with mock.patch("aurweb.config.set_option", side_effect=mock_set_option): main() assert stderr.getvalue().strip() == "error: no section found" diff --git a/test/test_cookies.py b/test/test_cookies.py new file mode 100644 index 00000000..dd4143cb --- /dev/null +++ b/test/test_cookies.py @@ -0,0 +1,151 @@ +from datetime import datetime +from http import HTTPStatus + +import pytest +from fastapi.testclient import TestClient + +from aurweb import config, db +from aurweb.asgi import app +from aurweb.models.account_type import USER_ID +from aurweb.models.user import User +from aurweb.testing.requests import Request + +# Some test global constants. +TEST_USERNAME = "test" +TEST_EMAIL = "test@example.org" +TEST_REFERER = { + "referer": config.get("options", "aur_location") + "/login", +} + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +@pytest.fixture +def client() -> TestClient: + client = TestClient(app=app) + + # Necessary for forged login CSRF protection on the login route. Set here + # instead of only on the necessary requests for convenience. + client.headers.update(TEST_REFERER) + + # disable redirects for our tests + client.follow_redirects = False + yield client + + +@pytest.fixture +def user() -> User: + with db.begin(): + user = db.create( + User, + Username=TEST_USERNAME, + Email=TEST_EMAIL, + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) + yield user + + +def test_cookies_login(client: TestClient, user: User): + # Log in with "Remember me" disabled + data = {"user": user.Username, "passwd": "testPassword", "next": "/"} + with client as request: + resp = request.post("/login", data=data) + + local_time = int(datetime.now().timestamp()) + expected_permanent = local_time + config.getint( + "options", "permanent_cookie_timeout" + ) + + # Check if we got permanent cookies with expected expiry date. + # Allow 1 second difference to account for timing issues + # between the request and current time. + # AURSID should be a session cookie (no expiry date) + assert "AURSID", "AURREMEMBER" in resp.cookies + for cookie in resp.cookies.jar: + if cookie.name == "AURSID": + assert cookie.expires is None + + if cookie.name == "AURREMEMBER": + assert abs(cookie.expires - expected_permanent) < 2 + assert cookie.value == "False" + + # Log out + with client as request: + request.cookies = resp.cookies + resp = request.post("/logout", data=data) + + # Make sure AURSID cookie is removed after logout + assert "AURSID" not in resp.cookies + + # Log in with "Remember me" enabled + data = { + "user": user.Username, + "passwd": "testPassword", + "next": "/", + "remember_me": "True", + } + with client as request: + resp = request.post("/login", data=data) + + # Check if we got a permanent cookie with expected expiry date. + # Allow 1 second difference to account for timing issues + # between the request and current time. + # AURSID should be a persistent cookie + expected_persistent = local_time + config.getint( + "options", "persistent_cookie_timeout" + ) + assert "AURSID", "AURREMEMBER" in resp.cookies + for cookie in resp.cookies.jar: + if cookie.name in "AURSID": + assert abs(cookie.expires - expected_persistent) < 2 + + if cookie.name == "AURREMEMBER": + assert abs(cookie.expires - expected_permanent) < 2 + assert cookie.value == "True" + + # log in again even though we already have a session + with client as request: + resp = request.post("/login", data=data) + + # we are logged in already and should have been redirected + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == "/" + + +def test_cookie_language(client: TestClient, user: User): + # Unauthenticated reqeuests should set AURLANG cookie + data = {"set_lang": "en", "next": "/"} + with client as request: + resp = request.post("/language", data=data) + + local_time = int(datetime.now().timestamp()) + expected_permanent = local_time + config.getint( + "options", "permanent_cookie_timeout" + ) + + # Make sure we got an AURLANG cookie + assert "AURLANG" in resp.cookies + assert resp.cookies.get("AURLANG") == "en" + + # Check if we got a permanent cookie with expected expiry date. + # Allow 1 second difference to account for timing issues + # between the request and current time. + for cookie in resp.cookies.jar: + if cookie.name in "AURLANG": + assert abs(cookie.expires - expected_permanent) < 2 + + # Login and change the language + # We should not get a cookie since we store + # our language setting in the DB anyways + sid = user.login(Request(), "testPassword") + data = {"set_lang": "en", "next": "/"} + with client as request: + request.cookies = {"AURSID": sid} + resp = request.post("/language", data=data) + + assert "AURLANG" not in resp.cookies diff --git a/test/test_db.py b/test/test_db.py index f36fff2c..22dbdd36 100644 --- a/test/test_db.py +++ b/test/test_db.py @@ -2,26 +2,27 @@ import os import re import sqlite3 import tempfile - from unittest import mock import pytest +from sqlalchemy.exc import OperationalError import aurweb.config import aurweb.initdb - from aurweb import db from aurweb.models.account_type import AccountType class Args: - """ Stub arguments used for running aurweb.initdb. """ + """Stub arguments used for running aurweb.initdb.""" + use_alembic = True verbose = True class DBCursor: - """ A fake database cursor object used in tests. """ + """A fake database cursor object used in tests.""" + items = [] def execute(self, *args, **kwargs): @@ -33,7 +34,8 @@ class DBCursor: class DBConnection: - """ A fake database connection object used in tests. """ + """A fake database connection object used in tests.""" + @staticmethod def cursor(): return DBCursor() @@ -44,7 +46,7 @@ class DBConnection: def make_temp_config(*replacements): - """ Generate a temporary config file with a set of replacements. + """Generate a temporary config file with a set of replacements. :param *replacements: A variable number of tuple regex replacement pairs :return: A tuple containing (temp directory, temp config file) @@ -85,13 +87,16 @@ def make_temp_config(*replacements): def make_temp_sqlite_config(): - return make_temp_config((r"backend = .*", "backend = sqlite"), - (r"name = .*", "name = /tmp/aurweb.sqlite3")) + return make_temp_config( + (r"backend = .*", "backend = sqlite"), + (r"name = .*", "name = /tmp/aurweb.sqlite3"), + ) def make_temp_mysql_config(): - return make_temp_config((r"backend = .*", "backend = mysql"), - (r"name = .*", "name = aurweb_test")) + return make_temp_config( + (r"backend = .*", "backend = mysql"), (r"name = .*", "name = aurweb_test") + ) @pytest.fixture(autouse=True) @@ -150,7 +155,7 @@ def test_sqlalchemy_unknown_backend(): def test_db_connects_without_fail(): - """ This only tests the actual config supplied to pytest. """ + """This only tests the actual config supplied to pytest.""" db.connect() @@ -222,3 +227,22 @@ def test_name_without_pytest_current_test(): with mock.patch.dict("os.environ", {}, clear=True): dbname = aurweb.db.name() assert dbname == aurweb.config.get("database", "name") + + +def test_retry_deadlock(): + @db.retry_deadlock + def func(): + raise OperationalError("Deadlock found", tuple(), "") + + with pytest.raises(OperationalError): + func() + + +@pytest.mark.asyncio +async def test_async_retry_deadlock(): + @db.async_retry_deadlock + async def func(): + raise OperationalError("Deadlock found", tuple(), "") + + with pytest.raises(OperationalError): + await func() diff --git a/test/test_dependency_type.py b/test/test_dependency_type.py index c5afd38d..e172782b 100644 --- a/test/test_dependency_type.py +++ b/test/test_dependency_type.py @@ -12,8 +12,7 @@ def setup(db_test): def test_dependency_types(): dep_types = ["depends", "makedepends", "checkdepends", "optdepends"] for dep_type in dep_types: - dependency_type = query(DependencyType, - DependencyType.Name == dep_type).first() + dependency_type = query(DependencyType, DependencyType.Name == dep_type).first() assert dependency_type is not None diff --git a/test/test_email.py b/test/test_email.py index 873feffe..81abd507 100644 --- a/test/test_email.py +++ b/test/test_email.py @@ -1,5 +1,4 @@ import io - from subprocess import PIPE, Popen import pytest @@ -23,7 +22,7 @@ def sendmail(from_: str, to_: str, content: str) -> Email: def test_email_glue(): - """ Test that Email.glue() decodes both base64 and decoded content. """ + """Test that Email.glue() decodes both base64 and decoded content.""" body = "Test email." sendmail("test@example.org", "test@example.org", body) assert Email.count() == 1 @@ -34,7 +33,7 @@ def test_email_glue(): def test_email_dump(): - """ Test that Email.dump() dumps a single email. """ + """Test that Email.dump() dumps a single email.""" body = "Test email." sendmail("test@example.org", "test@example.org", body) assert Email.count() == 1 @@ -46,7 +45,7 @@ def test_email_dump(): def test_email_dump_multiple(): - """ Test that Email.dump() dumps multiple emails. """ + """Test that Email.dump() dumps multiple emails.""" body = "Test email." sendmail("test@example.org", "test@example.org", body) sendmail("test2@example.org", "test2@example.org", body) diff --git a/test/test_filelock.py b/test/test_filelock.py index 70aa7580..c0580642 100644 --- a/test/test_filelock.py +++ b/test/test_filelock.py @@ -1,5 +1,4 @@ import py - from _pytest.logging import LogCaptureFixture from aurweb.testing.filelock import FileLock diff --git a/test/test_filters.py b/test/test_filters.py index 558911f5..ea0fa58b 100644 --- a/test/test_filters.py +++ b/test/test_filters.py @@ -1,12 +1,14 @@ -from datetime import datetime +from datetime import UTC, datetime from zoneinfo import ZoneInfo +import pytest + from aurweb import filters, time def test_timestamp_to_datetime(): ts = time.utcnow() - dt = datetime.utcfromtimestamp(int(ts)) + dt = datetime.fromtimestamp(ts, UTC) assert filters.timestamp_to_datetime(ts) == dt @@ -22,7 +24,7 @@ def test_number_format(): def test_extend_query(): - """ Test extension of a query via extend_query. """ + """Test extension of a query via extend_query.""" query = {"a": "b"} extended = filters.extend_query(query, ("a", "c"), ("b", "d")) assert extended.get("a") == "c" @@ -30,7 +32,22 @@ def test_extend_query(): def test_to_qs(): - """ Test conversion from a query dictionary to a query string. """ + """Test conversion from a query dictionary to a query string.""" query = {"a": "b", "c": [1, 2, 3]} qs = filters.to_qs(query) assert qs == "a=b&c=1&c=2&c=3" + + +@pytest.mark.parametrize( + "value, args, expected", + [ + ("", (), ""), + ("a", (), "a"), + ("a", (1,), "a"), + ("%s", ("a",), "a"), + ("%s", ("ab",), "ab"), + ("%s%d", ("a", 1), "a1"), + ], +) +def test_safe_format(value: str, args: tuple, expected: str): + assert filters.safe_format(value, *args) == expected diff --git a/test/test_git_archives.py b/test/test_git_archives.py new file mode 100644 index 00000000..c90706a4 --- /dev/null +++ b/test/test_git_archives.py @@ -0,0 +1,242 @@ +from http import HTTPStatus +from typing import Tuple +from unittest import mock + +import py +import pygit2 +import pytest +from fastapi.testclient import TestClient + +from aurweb import asgi, config, db +from aurweb.archives.spec.base import GitInfo, SpecBase +from aurweb.models import Package, PackageBase, User +from aurweb.scripts import git_archive +from aurweb.testing.requests import Request + + +@pytest.fixture +def mock_metadata_archive( + tmp_path: py.path.local, +) -> Tuple[py.path.local, py.path.local]: + metadata_path = tmp_path / "metadata.git" + + get_ = config.get + + def mock_config(section: str, option: str) -> str: + if section == "git-archive": + if option == "metadata-repo": + return str(metadata_path) + return get_(section, option) + + with mock.patch("aurweb.config.get", side_effect=mock_config): + yield metadata_path + + +@pytest.fixture +def mock_users_archive(tmp_path: py.path.local) -> py.path.local: + users_path = tmp_path / "users.git" + + get_ = config.get + + def mock_config(section: str, option: str) -> str: + if section == "git-archive": + if option == "users-repo": + return str(users_path) + return get_(section, option) + + with mock.patch("aurweb.config.get", side_effect=mock_config): + yield users_path + + +@pytest.fixture +def mock_pkgbases_archive(tmp_path: py.path.local) -> py.path.local: + pkgbases_path = tmp_path / "pkgbases.git" + + get_ = config.get + + def mock_config(section: str, option: str) -> str: + if section == "git-archive": + if option == "pkgbases-repo": + return str(pkgbases_path) + return get_(section, option) + + with mock.patch("aurweb.config.get", side_effect=mock_config): + yield pkgbases_path + + +@pytest.fixture +def mock_pkgnames_archive(tmp_path: py.path.local) -> py.path.local: + pkgnames_path = tmp_path / "pkgnames.git" + + get_ = config.get + + def mock_config(section: str, option: str) -> str: + if section == "git-archive": + if option == "pkgnames-repo": + return str(pkgnames_path) + return get_(section, option) + + with mock.patch("aurweb.config.get", side_effect=mock_config): + yield pkgnames_path + + +@pytest.fixture +def metadata(mock_metadata_archive: py.path.local) -> py.path.local: + args = [__name__, "--spec", "metadata"] + with mock.patch("sys.argv", args): + yield mock_metadata_archive + + +@pytest.fixture +def users(mock_users_archive: py.path.local) -> py.path.local: + args = [__name__, "--spec", "users"] + with mock.patch("sys.argv", args): + yield mock_users_archive + + +@pytest.fixture +def pkgbases(mock_pkgbases_archive: py.path.local) -> py.path.local: + args = [__name__, "--spec", "pkgbases"] + with mock.patch("sys.argv", args): + yield mock_pkgbases_archive + + +@pytest.fixture +def pkgnames(mock_pkgnames_archive: py.path.local) -> py.path.local: + args = [__name__, "--spec", "pkgnames"] + with mock.patch("sys.argv", args): + yield mock_pkgnames_archive + + +@pytest.fixture +def client() -> TestClient: + yield TestClient(app=asgi.app) + + +@pytest.fixture +def user(db_test: None) -> User: + with db.begin(): + user_ = db.create( + User, + Username="test", + Email="test@example.org", + Passwd="testPassword", + ) + + yield user_ + + +@pytest.fixture +def package(user: User) -> Package: + with db.begin(): + pkgbase_ = db.create( + PackageBase, + Name="test", + Maintainer=user, + Packager=user, + ) + + pkg_ = db.create( + Package, + PackageBase=pkgbase_, + Name="test", + ) + + yield pkg_ + + +def commit_count(repo: pygit2.Repository) -> int: + commits = 0 + for _ in repo.walk(repo.head.target): + commits += 1 + return commits + + +def test_specbase_raises_notimplementederror(): + spec = SpecBase() + with pytest.raises(NotImplementedError): + spec.generate() + + +def test_gitinfo_config(tmpdir: py.path.local): + path = tmpdir / "test.git" + git_info = GitInfo(path, {"user.name": "Test Person"}) + git_archive.init_repository(git_info) + + repo = pygit2.Repository(path) + assert repo.config["user.name"] == "Test Person" + + +def test_metadata(metadata: py.path.local, package: Package): + # Run main(), which creates mock_metadata_archive and commits current + # package data to it, exercising the "diff detected, committing" path + assert git_archive.main() == 0 + repo = pygit2.Repository(metadata) + assert commit_count(repo) == 1 + + # Run main() again to exercise the "no diff detected" path + assert git_archive.main() == 0 + repo = pygit2.Repository(metadata) + assert commit_count(repo) == 1 + + +def test_metadata_change( + client: TestClient, metadata: py.path.local, user: User, package: Package +): + """Test that metadata changes via aurweb cause git_archive to produce diffs.""" + # Run main(), which creates mock_metadata_archive and commits current + # package data to it, exercising the "diff detected, committing" path + assert git_archive.main() == 0 + repo = pygit2.Repository(metadata) + assert commit_count(repo) == 1 + + # Now, we modify `package`-related metadata via aurweb POST. + pkgbasename = package.PackageBase.Name + cookies = {"AURSID": user.login(Request(), "testPassword")} + + with client as request: + endp = f"/pkgbase/{pkgbasename}/keywords" + post_data = {"keywords": "abc def"} + request.cookies = cookies + resp = request.post(endp, data=post_data) + assert resp.status_code == HTTPStatus.OK + + # Run main() again, which should now produce a new commit with the + # keyword changes we just made + assert git_archive.main() == 0 + repo = pygit2.Repository(metadata) + assert commit_count(repo) == 2 + + +def test_metadata_delete(client: TestClient, metadata: py.path.local, package: Package): + # Run main(), which creates mock_metadata_archive and commits current + # package data to it, exercising the "diff detected, committing" path + assert git_archive.main() == 0 + repo = pygit2.Repository(metadata) + assert commit_count(repo) == 1 + + with db.begin(): + db.delete(package) + + # The deletion here should have caused a diff to be produced in git + assert git_archive.main() == 0 + repo = pygit2.Repository(metadata) + assert commit_count(repo) == 2 + + +def test_users(users: py.path.local, user: User): + assert git_archive.main() == 0 + repo = pygit2.Repository(users) + assert commit_count(repo) == 1 + + +def test_pkgbases(pkgbases: py.path.local, package: Package): + assert git_archive.main() == 0 + repo = pygit2.Repository(pkgbases) + assert commit_count(repo) == 1 + + +def test_pkgnames(pkgnames: py.path.local, package: Package): + assert git_archive.main() == 0 + repo = pygit2.Repository(pkgnames) + assert commit_count(repo) == 1 diff --git a/test/test_git_update.py b/test/test_git_update.py new file mode 100644 index 00000000..55db70b0 --- /dev/null +++ b/test/test_git_update.py @@ -0,0 +1,203 @@ +import json + +import pytest +from srcinfo import parse + +from aurweb.git.update import extract_arch_fields, parse_dep, size_humanize + +SRCINFO = """ +pkgbase = ponies +pkgdesc = Test parse +pkgver = 1.0.0 +pkgrel = 1 +url = https://example.com +arch = x86_64 +arch = aarch64 +arch = armv7h +license = GPL +depends = curl +depends = openssl +optdepends = unicorns: Extends ponies forehead with a horn +provides = horse +conflicts = horse +options = !strip +options = staticlibs +source = ponies.service +source = ponies.sysusers +source = ponies.tmpfiles +sha256sums = 9d8f9d73e5fa2b2877dde010c0d8ca6fbf47f03eb1f01b02f846026a949a0dcf +sha256sums = d005fcd009ec5404e1ec88246c31e664167f5551d6cabc35f68eb41750bfe590 +sha256sums = 64022e15565a609f449090f02d53ee90ef95cffec52ae14f99e4e2132b6cffe1 +source_x86_64 = filea +source_x86_64 = fileb +sha256sums_x86_64 = f486f8528292c067620e9d495f66b0af2ad55dd4dc2e9d35b11aa7dd656d487b +sha256sums_x86_64 = f486f8528292c067620e9d495f66b0af2ad55dd4dc2e9d35b11aa7dd656d487c +source_aarch64 = filex +sha256sums_aarch64 = 1f72deec0a9af5059e1350d7b5a5a93bc4d2fbef6eeaa363fda764eb9c472b7b +source_armv7h = filey +sha256sums_armv7h = 8229b4bbf43563d8b688d19a514fb0fa0a1ef0eadbd96233882a4b496fa4c8c8 +pkgname = ponies +""" + +EXPECTED = """ +{ + "packages": { + "ponies": {} + }, + "pkgbase": "ponies", + "pkgdesc": "Test parse", + "pkgver": "1.0.0", + "pkgrel": "1", + "url": "https://example.com", + "arch": [ + "x86_64", + "aarch64", + "armv7h" + ], + "license": [ + "GPL" + ], + "depends": [ + "curl", + "openssl" + ], + "optdepends": [ + "unicorns: Extends ponies forehead with a horn" + ], + "provides": [ + "horse" + ], + "conflicts": [ + "horse" + ], + "options": [ + "!strip", + "staticlibs" + ], + "source": [ + "ponies.service", + "ponies.sysusers", + "ponies.tmpfiles" + ], + "sha256sums": [ + "9d8f9d73e5fa2b2877dde010c0d8ca6fbf47f03eb1f01b02f846026a949a0dcf", + "d005fcd009ec5404e1ec88246c31e664167f5551d6cabc35f68eb41750bfe590", + "64022e15565a609f449090f02d53ee90ef95cffec52ae14f99e4e2132b6cffe1" + ], + "source_x86_64": [ + "filea", + "fileb" + ], + "sha256sums_x86_64": [ + "f486f8528292c067620e9d495f66b0af2ad55dd4dc2e9d35b11aa7dd656d487b", + "f486f8528292c067620e9d495f66b0af2ad55dd4dc2e9d35b11aa7dd656d487c" + ], + "source_aarch64": [ + "filex" + ], + "sha256sums_aarch64": [ + "1f72deec0a9af5059e1350d7b5a5a93bc4d2fbef6eeaa363fda764eb9c472b7b" + ], + "source_armv7h": [ + "filey" + ], + "sha256sums_armv7h": [ + "8229b4bbf43563d8b688d19a514fb0fa0a1ef0eadbd96233882a4b496fa4c8c8" + ] +} +""" + + +def test_srcinfo_parse(): + (info, error) = parse.parse_srcinfo(SRCINFO) + + assert not error + + # Check if parsing function returns what we expect + assert json.loads(EXPECTED) == info + + +def test_git_update_extract_arch_fields(): + (info, error) = parse.parse_srcinfo(SRCINFO) + + assert not error + + # check arrays + sources = extract_arch_fields(info, "source") + + # We expect 7 source files + assert len(sources) == 7 + + # First one should be our service file + assert sources[0]["value"] == "ponies.service" + + # add more... + + +@pytest.mark.parametrize( + "size, expected", + [ + (1024, "1024B"), + (1024.5, "1024.50B"), + (256000, "250.00KiB"), + (25600000, "24.41MiB"), + (2560000000, "2.38GiB"), + (2560000000000, "2.33TiB"), + (2560000000000000, "2.27PiB"), + (2560000000000000000, "2.22EiB"), + (2560000000000000000000, "2.17ZiB"), + (2560000000000000000000000, "2.12YiB"), + ], +) +def test_size_humanize(size: any, expected: str): + assert size_humanize(size) == expected + + +@pytest.mark.parametrize( + "depstring, exp_depname, exp_depdesc, exp_depcond", + [ + ( + "my-little-pony: optional kids support", + "my-little-pony", + "optional kids support", + "", + ), + ( + "my-little-pony>7", + "my-little-pony", + "", + ">7", + ), + ( + "my-little-pony=7", + "my-little-pony", + "", + "=7", + ), + ( + "my-little-pony<7", + "my-little-pony", + "", + "<7", + ), + ( + "my-little-pony=<7", + "my-little-pony", + "", + "=<7", + ), + ( + "my-little-pony>=7.1: optional kids support", + "my-little-pony", + "optional kids support", + ">=7.1", + ), + ], +) +def test_parse_dep( + depstring: str, exp_depname: str, exp_depdesc: str, exp_depcond: str +): + depname, depdesc, depcond = parse_dep(depstring) + assert depname == exp_depname + assert depdesc == exp_depdesc + assert depcond == exp_depcond diff --git a/test/test_group.py b/test/test_group.py index 82b82464..a1c563b6 100644 --- a/test/test_group.py +++ b/test/test_group.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db diff --git a/test/test_homepage.py b/test/test_homepage.py index 63b832e3..fc5eb17e 100644 --- a/test/test_homepage.py +++ b/test/test_homepage.py @@ -1,14 +1,13 @@ import re - from http import HTTPStatus from unittest.mock import patch import pytest - from fastapi.testclient import TestClient from aurweb import db, time from aurweb.asgi import app +from aurweb.aur_redis import redis_connection from aurweb.models.account_type import USER_ID from aurweb.models.package import Package from aurweb.models.package_base import PackageBase @@ -16,7 +15,6 @@ from aurweb.models.package_comaintainer import PackageComaintainer from aurweb.models.package_request import PackageRequest from aurweb.models.request_type import DELETION_ID, RequestType from aurweb.models.user import User -from aurweb.redis import redis_connection from aurweb.testing.html import parse_root from aurweb.testing.requests import Request @@ -31,16 +29,26 @@ def setup(db_test): @pytest.fixture def user(): with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - Passwd="testPassword", AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @pytest.fixture def user2(): with db.begin(): - user = db.create(User, Username="test2", Email="test2@example.org", - Passwd="testPassword", AccountTypeID=USER_ID) + user = db.create( + User, + Username="test2", + Email="test2@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @@ -50,10 +58,17 @@ def redis(): def delete_keys(): # Cleanup keys if they exist. - for key in ("package_count", "orphan_count", "user_count", - "trusted_user_count", "seven_days_old_added", - "seven_days_old_updated", "year_old_updated", - "never_updated", "package_updates"): + for key in ( + "package_count", + "orphan_count", + "user_count", + "package_maintainer_count", + "seven_days_old_added", + "seven_days_old_updated", + "year_old_updated", + "never_updated", + "package_updates", + ): if redis.get(key) is not None: redis.delete(key) @@ -66,16 +81,21 @@ def redis(): def package(user: User) -> Package: now = time.utcnow() with db.begin(): - pkgbase = db.create(PackageBase, Name="test-pkg", - Maintainer=user, Packager=user, - SubmittedTS=now, ModifiedTS=now) + pkgbase = db.create( + PackageBase, + Name="test-pkg", + Maintainer=user, + Packager=user, + SubmittedTS=now, + ModifiedTS=now, + ) pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) yield pkg @pytest.fixture def packages(user): - """ Yield a list of num_packages Package objects maintained by user. """ + """Yield a list of num_packages Package objects maintained by user.""" num_packages = 50 # Tunable # For i..num_packages, create a package named pkg_{i}. @@ -83,9 +103,14 @@ def packages(user): now = time.utcnow() with db.begin(): for i in range(num_packages): - pkgbase = db.create(PackageBase, Name=f"pkg_{i}", - Maintainer=user, Packager=user, - SubmittedTS=now, ModifiedTS=now) + pkgbase = db.create( + PackageBase, + Name=f"pkg_{i}", + Maintainer=user, + Packager=user, + SubmittedTS=now, + ModifiedTS=now, + ) pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) pkgs.append(pkg) now += 1 @@ -99,28 +124,48 @@ def test_homepage(): assert response.status_code == int(HTTPStatus.OK) -@patch('aurweb.util.get_ssh_fingerprints') -def test_homepage_ssh_fingerprints(get_ssh_fingerprints_mock): - fingerprints = {'Ed25519': "SHA256:RFzBCUItH9LZS0cKB5UE6ceAYhBD5C8GeOBip8Z11+4"} +@patch("aurweb.util.get_ssh_fingerprints") +def test_homepage_ssh_fingerprints(get_ssh_fingerprints_mock, user): + fingerprints = {"Ed25519": "SHA256:RFzBCUItH9LZS0cKB5UE6ceAYhBD5C8GeOBip8Z11+4"} get_ssh_fingerprints_mock.return_value = fingerprints + # without authentication (Home) with client as request: response = request.get("/") - for key, value in fingerprints.items(): - assert key in response.content.decode() - assert value in response.content.decode() - assert 'The following SSH fingerprints are used for the AUR' in response.content.decode() + # with authentication (Dashboard) + with client as auth_request: + auth_request.cookies = {"AURSID": user.login(Request(), "testPassword")} + auth_response = auth_request.get("/") + + for resp in [response, auth_response]: + for key, value in fingerprints.items(): + assert key in resp.content.decode() + assert value in resp.content.decode() + assert ( + "The following SSH fingerprints are used for the AUR" + in resp.content.decode() + ) -@patch('aurweb.util.get_ssh_fingerprints') -def test_homepage_no_ssh_fingerprints(get_ssh_fingerprints_mock): +@patch("aurweb.util.get_ssh_fingerprints") +def test_homepage_no_ssh_fingerprints(get_ssh_fingerprints_mock, user): get_ssh_fingerprints_mock.return_value = {} + # without authentication (Home) with client as request: response = request.get("/") - assert 'The following SSH fingerprints are used for the AUR' not in response.content.decode() + # with authentication (Dashboard) + with client as auth_request: + auth_request.cookies = {"AURSID": user.login(Request(), "testPassword")} + auth_response = auth_request.get("/") + + for resp in [response, auth_response]: + assert ( + "The following SSH fingerprints are used for the AUR" + not in resp.content.decode() + ) def test_homepage_stats(redis, packages): @@ -131,20 +176,20 @@ def test_homepage_stats(redis, packages): root = parse_root(response.text) expectations = [ - ("Packages", r'\d+'), - ("Orphan Packages", r'\d+'), - ("Packages added in the past 7 days", r'\d+'), - ("Packages updated in the past 7 days", r'\d+'), - ("Packages updated in the past year", r'\d+'), - ("Packages never updated", r'\d+'), - ("Registered Users", r'\d+'), - ("Trusted Users", r'\d+') + ("Packages", r"\d+"), + ("Orphan Packages", r"\d+"), + ("Packages added in the past 7 days", r"\d+"), + ("Packages updated in the past 7 days", r"\d+"), + ("Packages updated in the past year", r"\d+"), + ("Packages never updated", r"\d+"), + ("Registered Users", r"\d+"), + ("Package Maintainers", r"\d+"), ] stats = root.xpath('//div[@id="pkg-stats"]//tr') for i, expected in enumerate(expectations): expected_key, expected_regex = expected - key, value = stats[i].xpath('./td') + key, value = stats[i].xpath("./td") assert key.text.strip() == expected_key assert re.match(expected_regex, value.text.strip()) @@ -165,7 +210,7 @@ def test_homepage_updates(redis, packages): expectations = [f"pkg_{i}" for i in range(50 - 1, 50 - 1 - 15, -1)] updates = root.xpath('//div[@id="pkg-updates"]/table/tbody/tr') for i, expected in enumerate(expectations): - pkgname = updates[i].xpath('./td/a').pop(0) + pkgname = updates[i].xpath("./td/a").pop(0) assert pkgname.text.strip() == expected @@ -173,13 +218,14 @@ def test_homepage_dashboard(redis, packages, user): # Create Comaintainer records for all of the packages. with db.begin(): for pkg in packages: - db.create(PackageComaintainer, - PackageBase=pkg.PackageBase, - User=user, Priority=1) + db.create( + PackageComaintainer, PackageBase=pkg.PackageBase, User=user, Priority=1 + ) cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - response = request.get("/", cookies=cookies) + request.cookies = cookies + response = request.get("/") assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -189,16 +235,18 @@ def test_homepage_dashboard(redis, packages, user): expectations = [f"pkg_{i}" for i in range(50 - 1, 0, -1)] my_packages = root.xpath('//table[@id="my-packages"]/tbody/tr') for i, expected in enumerate(expectations): - name, version, votes, pop, voted, notify, desc, maint \ - = my_packages[i].xpath('./td') - assert name.xpath('./a').pop(0).text.strip() == expected + name, version, votes, pop, voted, notify, desc, maint = my_packages[i].xpath( + "./td" + ) + assert name.xpath("./a").pop(0).text.strip() == expected # Do the same for the Comaintained Packages table. my_packages = root.xpath('//table[@id="comaintained-packages"]/tbody/tr') for i, expected in enumerate(expectations): - name, version, votes, pop, voted, notify, desc, maint \ - = my_packages[i].xpath('./td') - assert name.xpath('./a').pop(0).text.strip() == expected + name, version, votes, pop, voted, notify, desc, maint = my_packages[i].xpath( + "./td" + ) + assert name.xpath("./a").pop(0).text.strip() == expected def test_homepage_dashboard_requests(redis, packages, user): @@ -207,20 +255,26 @@ def test_homepage_dashboard_requests(redis, packages, user): pkg = packages[0] reqtype = db.query(RequestType, RequestType.ID == DELETION_ID).first() with db.begin(): - pkgreq = db.create(PackageRequest, PackageBase=pkg.PackageBase, - PackageBaseName=pkg.PackageBase.Name, - User=user, Comments=str(), - ClosureComment=str(), RequestTS=now, - RequestType=reqtype) + pkgreq = db.create( + PackageRequest, + PackageBase=pkg.PackageBase, + PackageBaseName=pkg.PackageBase.Name, + User=user, + Comments=str(), + ClosureComment=str(), + RequestTS=now, + RequestType=reqtype, + ) cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - response = request.get("/", cookies=cookies) + request.cookies = cookies + response = request.get("/") assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) request = root.xpath('//table[@id="pkgreq-results"]/tbody/tr').pop(0) - pkgname = request.xpath('./td/a').pop(0) + pkgname = request.xpath("./td/a").pop(0) assert pkgname.text.strip() == pkgreq.PackageBaseName @@ -232,13 +286,14 @@ def test_homepage_dashboard_flagged_packages(redis, packages, user): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - response = request.get("/", cookies=cookies) + request.cookies = cookies + response = request.get("/") assert response.status_code == int(HTTPStatus.OK) # Check to see that the package showed up in the Flagged Packages table. root = parse_root(response.text) flagged_pkg = root.xpath('//table[@id="flagged-packages"]/tbody/tr').pop(0) - flagged_name = flagged_pkg.xpath('./td/a').pop(0) + flagged_name = flagged_pkg.xpath("./td/a").pop(0) assert flagged_name.text.strip() == pkg.Name @@ -247,8 +302,7 @@ def test_homepage_dashboard_flagged(user: User, user2: User, package: Package): now = time.utcnow() with db.begin(): - db.create(PackageComaintainer, User=user2, - PackageBase=pkgbase, Priority=1) + db.create(PackageComaintainer, User=user2, PackageBase=pkgbase, Priority=1) pkgbase.OutOfDateTS = now - 5 pkgbase.Flagger = user @@ -256,7 +310,8 @@ def test_homepage_dashboard_flagged(user: User, user2: User, package: Package): # flagged co-maintained packages. comaint_cookies = {"AURSID": user2.login(Request(), "testPassword")} with client as request: - resp = request.get("/", cookies=comaint_cookies) + request.cookies = comaint_cookies + resp = request.get("/") assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -267,7 +322,8 @@ def test_homepage_dashboard_flagged(user: User, user2: User, package: Package): # flagged maintained packages. cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get("/", cookies=cookies) + request.cookies = cookies + resp = request.get("/") assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) diff --git a/test/test_html.py b/test/test_html.py index ffe2a9f2..602ee26c 100644 --- a/test/test_html.py +++ b/test/test_html.py @@ -1,20 +1,19 @@ """ A test suite used to test HTML renders in different cases. """ + import hashlib import os import tempfile - from http import HTTPStatus from unittest import mock import fastapi import pytest - from fastapi import HTTPException from fastapi.testclient import TestClient from aurweb import asgi, config, db from aurweb.models import PackageBase -from aurweb.models.account_type import TRUSTED_USER_ID, USER_ID +from aurweb.models.account_type import PACKAGE_MAINTAINER_ID, USER_ID from aurweb.models.user import User from aurweb.testing.html import get_errors, get_successes, parse_root from aurweb.testing.requests import Request @@ -33,15 +32,20 @@ def client() -> TestClient: @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - Passwd="testPassword", AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @pytest.fixture -def trusted_user(user: User) -> User: +def package_maintainer(user: User) -> User: with db.begin(): - user.AccountTypeID = TRUSTED_USER_ID + user.AccountTypeID = PACKAGE_MAINTAINER_ID yield user @@ -53,12 +57,7 @@ def pkgbase(user: User) -> PackageBase: def test_archdev_navbar(client: TestClient): - expected = [ - "AUR Home", - "Packages", - "Register", - "Login" - ] + expected = ["AUR Home", "Packages", "Register", "Login"] with client as request: resp = request.get("/") assert resp.status_code == int(HTTPStatus.OK) @@ -70,16 +69,11 @@ def test_archdev_navbar(client: TestClient): def test_archdev_navbar_authenticated(client: TestClient, user: User): - expected = [ - "Dashboard", - "Packages", - "Requests", - "My Account", - "Logout" - ] + expected = ["Dashboard", "Packages", "Requests", "My Account", "Logout"] cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get("/", cookies=cookies) + request.cookies = cookies + resp = request.get("/") assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -88,20 +82,20 @@ def test_archdev_navbar_authenticated(client: TestClient, user: User): assert item.text.strip() == expected[i] -def test_archdev_navbar_authenticated_tu(client: TestClient, - trusted_user: User): +def test_archdev_navbar_authenticated_pm(client: TestClient, package_maintainer: User): expected = [ "Dashboard", "Packages", "Requests", "Accounts", "My Account", - "Trusted User", - "Logout" + "Package Maintainer", + "Logout", ] - cookies = {"AURSID": trusted_user.login(Request(), "testPassword")} + cookies = {"AURSID": package_maintainer.login(Request(), "testPassword")} with client as request: - resp = request.get("/", cookies=cookies) + request.cookies = cookies + resp = request.get("/") assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -131,7 +125,7 @@ def test_get_successes(): def test_archive_sig(client: TestClient): - hash_value = hashlib.sha256(b'test').hexdigest() + hash_value = hashlib.sha256(b"test").hexdigest() with tempfile.TemporaryDirectory() as tmpdir: packages_sha256 = os.path.join(tmpdir, "packages.gz.sha256") @@ -179,25 +173,23 @@ def test_disabled_metrics(client: TestClient): def test_rtl(client: TestClient): responses = {} - expected = [ - [], - [], - ['rtl'], - ['rtl'] - ] + expected = [[], [], ["rtl"], ["rtl"]] with client as request: responses["default"] = request.get("/") - responses["de"] = request.get("/", cookies={"AURLANG": "de"}) - responses["he"] = request.get("/", cookies={"AURLANG": "he"}) - responses["ar"] = request.get("/", cookies={"AURLANG": "ar"}) + request.cookies = {"AURLANG": "de"} + responses["de"] = request.get("/") + request.cookies = {"AURLANG": "he"} + responses["he"] = request.get("/") + request.cookies = {"AURLANG": "ar"} + responses["ar"] = request.get("/") for i, (lang, resp) in enumerate(responses.items()): assert resp.status_code == int(HTTPStatus.OK) t = parse_root(resp.text) - assert t.xpath('//html/@dir') == expected[i] + assert t.xpath("//html/@dir") == expected[i] def test_404_with_valid_pkgbase(client: TestClient, pkgbase: PackageBase): - """ Test HTTPException with status_code == 404 and valid pkgbase. """ + """Test HTTPException with status_code == 404 and valid pkgbase.""" endpoint = f"/{pkgbase.Name}" with client as request: response = request.get(endpoint) @@ -208,8 +200,8 @@ def test_404_with_valid_pkgbase(client: TestClient, pkgbase: PackageBase): assert "To clone the Git repository" in body -def test_404(client: TestClient): - """ Test HTTPException with status_code == 404 without a valid pkgbase. """ +def test_404(client: TestClient, user): + """Test HTTPException with status_code == 404 without a valid pkgbase.""" with client as request: response = request.get("/nonexistentroute") assert response.status_code == int(HTTPStatus.NOT_FOUND) @@ -219,9 +211,24 @@ def test_404(client: TestClient): # No `pkgbase` is provided here; we don't see the extra info. assert "To clone the Git repository" not in body + # Create a pkgbase named "pkgbase" + # Should NOT return extra info for "pkgbase" + with db.begin(): + db.create(PackageBase, Name="pkgbase", Maintainer=user) + + with client as request: + response = request.get("/pkgbase/doesnotexist") + assert response.status_code == int(HTTPStatus.NOT_FOUND) + + body = response.text + assert "404 - Page Not Found" in body + # No `pkgbase` is provided here; we don't see the extra info. + assert "To clone the Git repository" not in body + def test_503(client: TestClient): - """ Test HTTPException with status_code == 503 (Service Unavailable). """ + """Test HTTPException with status_code == 503 (Service Unavailable).""" + @asgi.app.get("/raise-503") async def raise_503(request: fastapi.Request): raise HTTPException(status_code=HTTPStatus.SERVICE_UNAVAILABLE) diff --git a/test/test_initdb.py b/test/test_initdb.py index 44681d8e..db5edf74 100644 --- a/test/test_initdb.py +++ b/test/test_initdb.py @@ -3,7 +3,6 @@ import pytest import aurweb.config import aurweb.db import aurweb.initdb - from aurweb.models.account_type import AccountType @@ -19,11 +18,11 @@ class Args: def test_run(): from aurweb.schema import metadata + aurweb.db.kill_engine() metadata.drop_all(aurweb.db.get_engine()) aurweb.initdb.run(Args()) # Check that constant table rows got added via initdb. - record = aurweb.db.query(AccountType, - AccountType.AccountType == "User").first() + record = aurweb.db.query(AccountType, AccountType.AccountType == "User").first() assert record is not None diff --git a/test/test_l10n.py b/test/test_l10n.py index c24c5f55..e1b5ac3f 100644 --- a/test/test_l10n.py +++ b/test/test_l10n.py @@ -1,36 +1,78 @@ """ Test our l10n module. """ -from aurweb import filters, l10n + +from aurweb import config, filters, l10n from aurweb.testing.requests import Request def test_translator(): - """ Test creating l10n translation tools. """ + """Test creating l10n translation tools.""" de_home = l10n.translator.translate("Home", "de") assert de_home == "Startseite" def test_get_request_language(): - """ First, tests default_lang, then tests a modified AURLANG cookie. """ + """Test getting the language setting from a request.""" + # Default language + default_lang = config.get("options", "default_lang") request = Request() - assert l10n.get_request_language(request) == "en" + assert l10n.get_request_language(request) == default_lang + # Language setting from cookie: de request.cookies["AURLANG"] = "de" assert l10n.get_request_language(request) == "de" + # Language setting from cookie: nonsense + # Should fallback to default lang + request.cookies["AURLANG"] = "nonsense" + assert l10n.get_request_language(request) == default_lang + + # Language setting from query param: de + request.cookies = {} + request.query_params = {"language": "de"} + assert l10n.get_request_language(request) == "de" + + # Language setting from query param: nonsense + # Should fallback to default lang + request.query_params = {"language": "nonsense"} + assert l10n.get_request_language(request) == default_lang + + # Language setting from query param: de and cookie + # Query param should have precedence + request.query_params = {"language": "de"} + request.cookies["AURLANG"] = "fr" + assert l10n.get_request_language(request) == "de" + + # Language setting from authenticated user + request.cookies = {} + request.query_params = {} + request.user.authenticated = True + request.user.LangPreference = "de" + assert l10n.get_request_language(request) == "de" + + # Language setting from authenticated user with query param + # Query param should have precedence + request.query_params = {"language": "fr"} + assert l10n.get_request_language(request) == "fr" + + # Language setting from authenticated user with cookie + # DB setting should have precedence + request.query_params = {} + request.cookies["AURLANG"] = "fr" + assert l10n.get_request_language(request) == "de" + def test_get_raw_translator_for_request(): - """ Make sure that get_raw_translator_for_request is giving us - the translator we expect. """ + """Make sure that get_raw_translator_for_request is giving us + the translator we expect.""" request = Request() request.cookies["AURLANG"] = "de" translator = l10n.get_raw_translator_for_request(request) - assert translator.gettext("Home") == \ - l10n.translator.translate("Home", "de") + assert translator.gettext("Home") == l10n.translator.translate("Home", "de") def test_get_translator_for_request(): - """ Make sure that get_translator_for_request is giving us back - our expected translation function. """ + """Make sure that get_translator_for_request is giving us back + our expected translation function.""" request = Request() request.cookies["AURLANG"] = "de" @@ -43,10 +85,8 @@ def test_tn_filter(): request.cookies["AURLANG"] = "en" context = {"language": "en", "request": request} - translated = filters.tn(context, 1, "%d package found.", - "%d packages found.") + translated = filters.tn(context, 1, "%d package found.", "%d packages found.") assert translated == "%d package found." - translated = filters.tn(context, 2, "%d package found.", - "%d packages found.") + translated = filters.tn(context, 2, "%d package found.", "%d packages found.") assert translated == "%d packages found." diff --git a/test/test_license.py b/test/test_license.py index b34bd260..cea76e7d 100644 --- a/test/test_license.py +++ b/test/test_license.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db diff --git a/test/test_logging.py b/test/test_logging.py index 63092d07..90d13c93 100644 --- a/test/test_logging.py +++ b/test/test_logging.py @@ -1,6 +1,6 @@ -from aurweb import logging +from aurweb import aur_logging -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) def test_logging(caplog): diff --git a/test/test_metrics.py b/test/test_metrics.py new file mode 100644 index 00000000..c9f3d617 --- /dev/null +++ b/test/test_metrics.py @@ -0,0 +1,39 @@ +import pytest +from prometheus_client import REGISTRY, generate_latest + +from aurweb import db +from aurweb.cache import db_query_cache +from aurweb.models.account_type import USER_ID +from aurweb.models.user import User + + +@pytest.fixture(autouse=True) +def setup(db_test, prometheus_test): + return + + +@pytest.fixture +def user() -> User: + with db.begin(): + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) + yield user + + +def test_search_cache_metrics(user: User): + # Fire off 3 identical queries for caching + for _ in range(3): + db_query_cache("key", db.query(User)) + + # Get metrics + metrics = str(generate_latest(REGISTRY)) + + # We should have 1 miss and 2 hits + assert 'search_requests_total{cache="miss"} 1.0' in metrics + assert 'search_requests_total{cache="hit"} 2.0' in metrics diff --git a/test/test_mkpkglists.py b/test/test_mkpkglists.py index 7b538e02..310d9b04 100644 --- a/test/test_mkpkglists.py +++ b/test/test_mkpkglists.py @@ -1,15 +1,21 @@ import gzip import json import os - -from typing import List from unittest import mock import py import pytest from aurweb import config, db -from aurweb.models import License, Package, PackageBase, PackageDependency, PackageLicense, User +from aurweb.models import ( + License, + Package, + PackageBase, + PackageComaintainer, + PackageDependency, + PackageLicense, + User, +) from aurweb.models.account_type import USER_ID from aurweb.models.dependency_type import DEPENDS_ID @@ -25,6 +31,7 @@ META_KEYS = [ "Popularity", "OutOfDate", "Maintainer", + "Submitter", "FirstSubmitted", "LastModified", "URLPath", @@ -39,30 +46,41 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", - Email="test@example.org", - Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @pytest.fixture -def packages(user: User) -> List[Package]: +def packages(user: User) -> list[Package]: output = [] with db.begin(): lic = db.create(License, Name="GPL") for i in range(5): # Create the package. - pkgbase = db.create(PackageBase, Name=f"pkgbase_{i}", - Packager=user) - pkg = db.create(Package, PackageBase=pkgbase, - Name=f"pkg_{i}") + pkgbase = db.create( + PackageBase, + Name=f"pkgbase_{i}", + Packager=user, + Submitter=user, + ) + pkg = db.create(Package, PackageBase=pkgbase, Name=f"pkg_{i}") # Create some related records. db.create(PackageLicense, Package=pkg, License=lic) - db.create(PackageDependency, DepTypeID=DEPENDS_ID, - Package=pkg, DepName=f"dep_{i}", - DepCondition=">=1.0") + db.create( + PackageDependency, + DepTypeID=DEPENDS_ID, + Package=pkg, + DepName=f"dep_{i}", + DepCondition=">=1.0", + ) + db.create(PackageComaintainer, User=user, PackageBase=pkgbase, Priority=1) # Add the package to our output list. output.append(pkg) @@ -89,8 +107,11 @@ def config_mock(tmpdir: py.path.local) -> None: config.rehash() -def test_mkpkglists(tmpdir: py.path.local, config_mock: None, user: User, packages: List[Package]): +def test_mkpkglists( + tmpdir: py.path.local, config_mock: None, user: User, packages: list[Package] +): from aurweb.scripts import mkpkglists + mkpkglists.main() PACKAGES = config.get("mkpkglists", "packagesfile") @@ -107,13 +128,10 @@ def test_mkpkglists(tmpdir: py.path.local, config_mock: None, user: User, packag PKGBASE, "pkgbase_0\npkgbase_1\npkgbase_2\npkgbase_3\npkgbase_4\n", ), - ( - USERS, - "test\n" - ), + (USERS, "test\n"), ] - for (file, expected_content) in expectations: + for file, expected_content in expectations: with gzip.open(file, "r") as f: file_content = f.read().decode() assert file_content == expected_content @@ -137,6 +155,7 @@ def test_mkpkglists(tmpdir: py.path.local, config_mock: None, user: User, packag @mock.patch("sys.argv", ["mkpkglists", "--extended"]) def test_mkpkglists_extended_empty(config_mock: None): from aurweb.scripts import mkpkglists + mkpkglists.main() PACKAGES = config.get("mkpkglists", "packagesfile") @@ -153,7 +172,7 @@ def test_mkpkglists_extended_empty(config_mock: None): (META_EXT, "[\n]"), ] - for (file, expected_content) in expectations: + for file, expected_content in expectations: with gzip.open(file, "r") as f: file_content = f.read().decode() assert file_content == expected_content, f"{file=} contents malformed" @@ -167,9 +186,9 @@ def test_mkpkglists_extended_empty(config_mock: None): @mock.patch("sys.argv", ["mkpkglists", "--extended"]) -def test_mkpkglists_extended(config_mock: None, user: User, - packages: List[Package]): +def test_mkpkglists_extended(config_mock: None, user: User, packages: list[Package]): from aurweb.scripts import mkpkglists + mkpkglists.main() PACKAGES = config.get("mkpkglists", "packagesfile") @@ -187,13 +206,10 @@ def test_mkpkglists_extended(config_mock: None, user: User, PKGBASE, "pkgbase_0\npkgbase_1\npkgbase_2\npkgbase_3\npkgbase_4\n", ), - ( - USERS, - "test\n" - ), + (USERS, "test\n"), ] - for (file, expected_content) in expectations: + for file, expected_content in expectations: with gzip.open(file, "r") as f: file_content = f.read().decode() assert file_content == expected_content @@ -215,6 +231,7 @@ def test_mkpkglists_extended(config_mock: None, user: User, assert key in pkg, f"{pkg=} record does not have {key=}" assert isinstance(pkg["Depends"], list) assert isinstance(pkg["License"], list) + assert isinstance(pkg["CoMaintainers"], list) for file in (PACKAGES, PKGBASE, USERS, META, META_EXT): with open(f"{file}.sha256") as f: diff --git a/test/test_notify.py b/test/test_notify.py index fdec5ed7..3d773bc2 100644 --- a/test/test_notify.py +++ b/test/test_notify.py @@ -1,12 +1,11 @@ from logging import ERROR -from typing import List from unittest import mock import pytest from aurweb import config, db, models, time from aurweb.models import Package, PackageBase, PackageRequest, User -from aurweb.models.account_type import TRUSTED_USER_ID, USER_ID +from aurweb.models.account_type import PACKAGE_MAINTAINER_ID, USER_ID from aurweb.models.request_type import ORPHAN_ID from aurweb.scripts import notify, rendercomment from aurweb.testing.email import Email @@ -24,76 +23,102 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - Passwd=str(), AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + Passwd=str(), + AccountTypeID=USER_ID, + ) yield user @pytest.fixture def user1() -> User: with db.begin(): - user1 = db.create(User, Username="user1", Email="user1@example.org", - Passwd=str(), AccountTypeID=USER_ID) + user1 = db.create( + User, + Username="user1", + Email="user1@example.org", + Passwd=str(), + AccountTypeID=USER_ID, + ) yield user1 @pytest.fixture def user2() -> User: with db.begin(): - user2 = db.create(User, Username="user2", Email="user2@example.org", - Passwd=str(), AccountTypeID=USER_ID) + user2 = db.create( + User, + Username="user2", + Email="user2@example.org", + Passwd=str(), + AccountTypeID=USER_ID, + ) yield user2 @pytest.fixture -def pkgbases(user: User) -> List[PackageBase]: +def pkgbases(user: User) -> list[PackageBase]: now = time.utcnow() output = [] with db.begin(): for i in range(5): output.append( - db.create(PackageBase, Name=f"pkgbase_{i}", - Maintainer=user, SubmittedTS=now, - ModifiedTS=now)) - db.create(models.PackageNotification, PackageBase=output[-1], - User=user) + db.create( + PackageBase, + Name=f"pkgbase_{i}", + Maintainer=user, + SubmittedTS=now, + ModifiedTS=now, + ) + ) + db.create(models.PackageNotification, PackageBase=output[-1], User=user) yield output @pytest.fixture -def pkgreq(user2: User, pkgbases: List[PackageBase]): +def pkgreq(user2: User, pkgbases: list[PackageBase]): pkgbase = pkgbases[0] with db.begin(): - pkgreq_ = db.create(PackageRequest, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, User=user2, - ReqTypeID=ORPHAN_ID, - Comments="This is a request test comment.", - ClosureComment=str()) + pkgreq_ = db.create( + PackageRequest, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + User=user2, + ReqTypeID=ORPHAN_ID, + Comments="This is a request test comment.", + ClosureComment=str(), + ) yield pkgreq_ @pytest.fixture -def packages(pkgbases: List[PackageBase]) -> List[Package]: +def packages(pkgbases: list[PackageBase]) -> list[Package]: output = [] with db.begin(): for i, pkgbase in enumerate(pkgbases): output.append( - db.create(Package, PackageBase=pkgbase, - Name=f"pkg_{i}", Version=f"{i}.0")) + db.create( + Package, PackageBase=pkgbase, Name=f"pkg_{i}", Version=f"{i}.0" + ) + ) yield output -def test_out_of_date(user: User, user1: User, user2: User, - pkgbases: List[PackageBase]): +def test_out_of_date(user: User, user1: User, user2: User, pkgbases: list[PackageBase]): pkgbase = pkgbases[0] # Create two comaintainers. We'll pass the maintainer uid to # FlagNotification, so we should expect to get two emails. with db.begin(): - db.create(models.PackageComaintainer, - PackageBase=pkgbase, User=user1, Priority=1) - db.create(models.PackageComaintainer, - PackageBase=pkgbase, User=user2, Priority=2) + db.create( + models.PackageComaintainer, PackageBase=pkgbase, User=user1, Priority=1 + ) + db.create( + models.PackageComaintainer, PackageBase=pkgbase, User=user2, Priority=2 + ) # Send the notification for pkgbases[0]. notif = notify.FlagNotification(user.ID, pkgbases[0].ID) @@ -102,20 +127,20 @@ def test_out_of_date(user: User, user1: User, user2: User, # Should've gotten three emails: maintainer + the two comaintainers. assert Email.count() == 3 - # Comaintainer 1. + # Maintainer. first = Email(1).parse() - assert first.headers.get("To") == user1.Email + assert first.headers.get("To") == user.Email expected = f"AUR Out-of-date Notification for {pkgbase.Name}" assert first.headers.get("Subject") == expected - # Comaintainer 2. + # Comaintainer 1. second = Email(2).parse() - assert second.headers.get("To") == user2.Email + assert second.headers.get("To") == user1.Email - # Maintainer. + # Comaintainer 2. third = Email(3).parse() - assert third.headers.get("To") == user.Email + assert third.headers.get("To") == user2.Email def test_reset(user: User): @@ -162,12 +187,16 @@ link does not work, try copying and pasting it into your browser. assert email.body == expected -def test_comment(user: User, user2: User, pkgbases: List[PackageBase]): +def test_comment(user: User, user2: User, pkgbases: list[PackageBase]): pkgbase = pkgbases[0] with db.begin(): - comment = db.create(models.PackageComment, PackageBase=pkgbase, - User=user2, Comments="This is a test comment.") + comment = db.create( + models.PackageComment, + PackageBase=pkgbase, + User=user2, + Comments="This is a test comment.", + ) rendercomment.update_comment_render_fastapi(comment) notif = notify.CommentNotification(user2.ID, pkgbase.ID, comment.ID) @@ -194,7 +223,7 @@ please go to the package page [2] and select "Disable notifications". assert expected == email.body -def test_update(user: User, user2: User, pkgbases: List[PackageBase]): +def test_update(user: User, user2: User, pkgbases: list[PackageBase]): pkgbase = pkgbases[0] with db.begin(): user.UpdateNotify = 1 @@ -221,7 +250,7 @@ please go to the package page [2] and select "Disable notifications". assert expected == email.body -def test_adopt(user: User, user2: User, pkgbases: List[PackageBase]): +def test_adopt(user: User, user2: User, pkgbases: list[PackageBase]): pkgbase = pkgbases[0] notif = notify.AdoptNotification(user2.ID, pkgbase.ID) notif.send() @@ -241,7 +270,7 @@ The package {pkgbase.Name} [1] was adopted by {user2.Username} [2]. assert email.body == expected -def test_disown(user: User, user2: User, pkgbases: List[PackageBase]): +def test_disown(user: User, user2: User, pkgbases: list[PackageBase]): pkgbase = pkgbases[0] notif = notify.DisownNotification(user2.ID, pkgbase.ID) notif.send() @@ -261,7 +290,7 @@ The package {pkgbase.Name} [1] was disowned by {user2.Username} [2]. assert email.body == expected -def test_comaintainer_addition(user: User, pkgbases: List[PackageBase]): +def test_comaintainer_addition(user: User, pkgbases: list[PackageBase]): pkgbase = pkgbases[0] notif = notify.ComaintainerAddNotification(user.ID, pkgbase.ID) notif.send() @@ -280,7 +309,7 @@ You were added to the co-maintainer list of {pkgbase.Name} [1]. assert email.body == expected -def test_comaintainer_removal(user: User, pkgbases: List[PackageBase]): +def test_comaintainer_removal(user: User, pkgbases: list[PackageBase]): pkgbase = pkgbases[0] notif = notify.ComaintainerRemoveNotification(user.ID, pkgbase.ID) notif.send() @@ -299,7 +328,7 @@ You were removed from the co-maintainer list of {pkgbase.Name} [1]. assert email.body == expected -def test_suspended_ownership_change(user: User, pkgbases: List[PackageBase]): +def test_suspended_ownership_change(user: User, pkgbases: list[PackageBase]): with db.begin(): user.Suspended = 1 @@ -314,7 +343,7 @@ def test_suspended_ownership_change(user: User, pkgbases: List[PackageBase]): assert Email.count() == 1 -def test_delete(user: User, user2: User, pkgbases: List[PackageBase]): +def test_delete(user: User, user2: User, pkgbases: list[PackageBase]): pkgbase = pkgbases[0] notif = notify.DeleteNotification(user2.ID, pkgbase.ID) notif.send() @@ -336,7 +365,7 @@ You will no longer receive notifications about this package. assert email.body == expected -def test_merge(user: User, user2: User, pkgbases: List[PackageBase]): +def test_merge(user: User, user2: User, pkgbases: list[PackageBase]): source, target = pkgbases[:2] notif = notify.DeleteNotification(user2.ID, source.ID, target.ID) notif.send() @@ -361,21 +390,22 @@ please go to [3] and click "Disable notifications". assert email.body == expected -def set_tu(users: List[User]) -> User: +def set_pm(users: list[User]) -> User: with db.begin(): for user in users: - user.AccountTypeID = TRUSTED_USER_ID + user.AccountTypeID = PACKAGE_MAINTAINER_ID -def test_open_close_request(user: User, user2: User, - pkgreq: PackageRequest, - pkgbases: List[PackageBase]): - set_tu([user]) +def test_open_close_request( + user: User, user2: User, pkgreq: PackageRequest, pkgbases: list[PackageBase] +): + set_pm([user]) pkgbase = pkgbases[0] # Send an open request notification. notif = notify.RequestOpenNotification( - user2.ID, pkgreq.ID, pkgreq.RequestType.Name, pkgbase.ID) + user2.ID, pkgreq.ID, pkgreq.RequestType.Name, pkgbase.ID + ) notif.send() assert Email.count() == 1 @@ -421,22 +451,24 @@ Request #{pkgreq.ID} has been rejected by {user2.Username} [1]. email = Email(3).parse() assert email.headers.get("To") == aur_request_ml assert email.headers.get("Cc") == ", ".join([user.Email, user2.Email]) - expected = (f"[PRQ#{pkgreq.ID}] Orphan Request for " - f"{pkgbase.Name} Accepted") + expected = f"[PRQ#{pkgreq.ID}] Orphan Request for " f"{pkgbase.Name} Accepted" assert email.headers.get("Subject") == expected - expected = (f"Request #{pkgreq.ID} has been accepted automatically " - "by the Arch User Repository\npackage request system.") + expected = ( + f"Request #{pkgreq.ID} has been accepted automatically " + "by the Arch User Repository\npackage request system." + ) assert email.body == expected -def test_close_request_comaintainer_cc(user: User, user2: User, - pkgreq: PackageRequest, - pkgbases: List[PackageBase]): +def test_close_request_comaintainer_cc( + user: User, user2: User, pkgreq: PackageRequest, pkgbases: list[PackageBase] +): pkgbase = pkgbases[0] with db.begin(): - db.create(models.PackageComaintainer, PackageBase=pkgbase, - User=user2, Priority=1) + db.create( + models.PackageComaintainer, PackageBase=pkgbase, User=user2, Priority=1 + ) notif = notify.RequestCloseNotification(0, pkgreq.ID, "accepted") notif.send() @@ -447,9 +479,47 @@ def test_close_request_comaintainer_cc(user: User, user2: User, assert email.headers.get("Cc") == ", ".join([user.Email, user2.Email]) -def test_close_request_closure_comment(user: User, user2: User, - pkgreq: PackageRequest, - pkgbases: List[PackageBase]): +def test_open_close_request_hidden_email( + user2: User, pkgreq: PackageRequest, pkgbases: list[PackageBase] +): + pkgbase = pkgbases[0] + + # Enable the "HideEmail" option for our requester + with db.begin(): + user2.HideEmail = 1 + + # Send an open request notification. + notif = notify.RequestOpenNotification( + user2.ID, pkgreq.ID, pkgreq.RequestType.Name, pkgbase.ID + ) + + # Make sure our address got added to the bcc list + assert user2.Email in notif.get_bcc() + + notif.send() + assert Email.count() == 1 + + email = Email(1).parse() + # Make sure we don't have our address in the Cc header + assert user2.Email not in email.headers.get("Cc") + + # Create a closure notification on the pkgbase we just opened. + notif = notify.RequestCloseNotification(user2.ID, pkgreq.ID, "rejected") + + # Make sure our address got added to the bcc list + assert user2.Email in notif.get_bcc() + + notif.send() + assert Email.count() == 2 + + email = Email(2).parse() + # Make sure we don't have our address in the Cc header + assert user2.Email not in email.headers.get("Cc") + + +def test_close_request_closure_comment( + user: User, user2: User, pkgreq: PackageRequest, pkgbases: list[PackageBase] +): pkgbase = pkgbases[0] with db.begin(): pkgreq.ClosureComment = "This is a test closure comment." @@ -474,34 +544,34 @@ This is a test closure comment. assert email.body == expected -def test_tu_vote_reminders(user: User): - set_tu([user]) +def test_vote_reminders(user: User): + set_pm([user]) vote_id = 1 - notif = notify.TUVoteReminderNotification(vote_id) + notif = notify.VoteReminderNotification(vote_id) notif.send() assert Email.count() == 1 email = Email(1).parse() assert email.headers.get("To") == user.Email - expected = f"TU Vote Reminder: Proposal {vote_id}" + expected = f"Package Maintainer Vote Reminder: Proposal {vote_id}" assert email.headers.get("Subject") == expected expected = f"""\ Please remember to cast your vote on proposal {vote_id} [1]. The voting period ends in less than 48 hours. -[1] {aur_location}/tu/?id={vote_id}\ +[1] {aur_location}/package-maintainer/?id={vote_id}\ """ assert email.body == expected def test_notify_main(user: User): - """ Test TU vote reminder through aurweb.notify.main(). """ - set_tu([user]) + """Test PM vote reminder through aurweb.notify.main().""" + set_pm([user]) vote_id = 1 - args = ["aurweb-notify", "tu-vote-reminder", str(vote_id)] + args = ["aurweb-notify", "vote-reminder", str(vote_id)] with mock.patch("sys.argv", args): notify.main() @@ -509,14 +579,14 @@ def test_notify_main(user: User): email = Email(1).parse() assert email.headers.get("To") == user.Email - expected = f"TU Vote Reminder: Proposal {vote_id}" + expected = f"Package Maintainer Vote Reminder: Proposal {vote_id}" assert email.headers.get("Subject") == expected expected = f"""\ Please remember to cast your vote on proposal {vote_id} [1]. The voting period ends in less than 48 hours. -[1] {aur_location}/tu/?id={vote_id}\ +[1] {aur_location}/package-maintainer/?id={vote_id}\ """ assert email.body == expected @@ -540,6 +610,7 @@ def mock_smtp_config(cls): elif key == "smtp-password": return cls() return cls(config_get(section, key)) + return _mock_smtp_config @@ -575,6 +646,7 @@ def mock_smtp_starttls_config(cls): elif key == "smtp-password": return cls("password") return cls(config_get(section, key)) + return _mock_smtp_starttls_config @@ -591,8 +663,7 @@ def test_smtp_starttls(user: User): get = "aurweb.config.get" getboolean = "aurweb.config.getboolean" with mock.patch(get, side_effect=mock_smtp_starttls_config(str)): - with mock.patch( - getboolean, side_effect=mock_smtp_starttls_config(bool)): + with mock.patch(getboolean, side_effect=mock_smtp_starttls_config(bool)): with mock.patch("smtplib.SMTP", side_effect=smtp): notif = notify.WelcomeNotification(user.ID) notif.send() @@ -622,6 +693,7 @@ def mock_smtp_ssl_config(cls): elif key == "smtp-password": return cls("password") return cls(config_get(section, key)) + return _mock_smtp_ssl_config @@ -652,7 +724,7 @@ def test_notification_defaults(): def test_notification_oserror(user: User, caplog: pytest.LogCaptureFixture): - """ Try sending a notification with a bad SMTP configuration. """ + """Try sending a notification with a bad SMTP configuration.""" caplog.set_level(ERROR) config_get = config.get config_getint = config.getint diff --git a/test/test_official_provider.py b/test/test_official_provider.py index 9287ea2d..b36fff5a 100644 --- a/test/test_official_provider.py +++ b/test/test_official_provider.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -13,10 +12,12 @@ def setup(db_test): def test_official_provider_creation(): with db.begin(): - oprovider = db.create(OfficialProvider, - Name="some-name", - Repo="some-repo", - Provides="some-provides") + oprovider = db.create( + OfficialProvider, + Name="some-name", + Repo="some-repo", + Provides="some-provides", + ) assert bool(oprovider.ID) assert oprovider.Name == "some-name" assert oprovider.Repo == "some-repo" @@ -24,19 +25,23 @@ def test_official_provider_creation(): def test_official_provider_cs(): - """ Test case sensitivity of the database table. """ + """Test case sensitivity of the database table.""" with db.begin(): - oprovider = db.create(OfficialProvider, - Name="some-name", - Repo="some-repo", - Provides="some-provides") + oprovider = db.create( + OfficialProvider, + Name="some-name", + Repo="some-repo", + Provides="some-provides", + ) assert bool(oprovider.ID) with db.begin(): - oprovider_cs = db.create(OfficialProvider, - Name="SOME-NAME", - Repo="SOME-REPO", - Provides="SOME-PROVIDES") + oprovider_cs = db.create( + OfficialProvider, + Name="SOME-NAME", + Repo="SOME-REPO", + Provides="SOME-PROVIDES", + ) assert bool(oprovider_cs.ID) assert oprovider.ID != oprovider_cs.ID diff --git a/test/test_package.py b/test/test_package.py index 1408a182..83dd8d54 100644 --- a/test/test_package.py +++ b/test/test_package.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy import and_ from sqlalchemy.exc import IntegrityError @@ -9,8 +8,6 @@ from aurweb.models.package import Package from aurweb.models.package_base import PackageBase from aurweb.models.user import User -user = pkgbase = package = None - @pytest.fixture(autouse=True) def setup(db_test): @@ -20,20 +17,28 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @pytest.fixture def package(user: User) -> Package: with db.begin(): - pkgbase = db.create(PackageBase, Name="beautiful-package", - Maintainer=user) - package = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, - Description="Test description.", - URL="https://test.package") + pkgbase = db.create(PackageBase, Name="beautiful-package", Maintainer=user) + package = db.create( + Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="Test description.", + URL="https://test.package", + ) yield package @@ -48,21 +53,28 @@ def test_package(package: Package): package.Version = "1.2.3" # Make sure it got updated in the database. - record = db.query(Package).filter( - and_(Package.ID == package.ID, - Package.Version == "1.2.3") - ).first() + record = ( + db.query(Package) + .filter(and_(Package.ID == package.ID, Package.Version == "1.2.3")) + .first() + ) assert record is not None def test_package_null_pkgbase_raises(): with pytest.raises(IntegrityError): - Package(Name="some-package", Description="Some description.", - URL="https://some.package") + Package( + Name="some-package", + Description="Some description.", + URL="https://some.package", + ) def test_package_null_name_raises(package: Package): pkgbase = package.PackageBase with pytest.raises(IntegrityError): - Package(PackageBase=pkgbase, Description="Some description.", - URL="https://some.package") + Package( + PackageBase=pkgbase, + Description="Some description.", + URL="https://some.package", + ) diff --git a/test/test_package_base.py b/test/test_package_base.py index 5be7e40b..feea8183 100644 --- a/test/test_package_base.py +++ b/test/test_package_base.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -16,17 +15,21 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @pytest.fixture def pkgbase(user: User) -> PackageBase: with db.begin(): - pkgbase = db.create(PackageBase, Name="beautiful-package", - Maintainer=user) + pkgbase = db.create(PackageBase, Name="beautiful-package", Maintainer=user) yield pkgbase @@ -44,7 +47,7 @@ def test_package_base(user: User, pkgbase: PackageBase): def test_package_base_ci(user: User, pkgbase: PackageBase): - """ Test case insensitivity of the database table. """ + """Test case insensitivity of the database table.""" with pytest.raises(IntegrityError): with db.begin(): db.create(PackageBase, Name=pkgbase.Name.upper(), Maintainer=user) diff --git a/test/test_package_blacklist.py b/test/test_package_blacklist.py index 427c3be4..44de1830 100644 --- a/test/test_package_blacklist.py +++ b/test/test_package_blacklist.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db diff --git a/test/test_package_comaintainer.py b/test/test_package_comaintainer.py index e377edc0..52075887 100644 --- a/test/test_package_comaintainer.py +++ b/test/test_package_comaintainer.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -17,9 +16,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @@ -32,8 +36,9 @@ def pkgbase(user: User) -> PackageBase: def test_package_comaintainer_creation(user: User, pkgbase: PackageBase): with db.begin(): - package_comaintainer = db.create(PackageComaintainer, User=user, - PackageBase=pkgbase, Priority=5) + package_comaintainer = db.create( + PackageComaintainer, User=user, PackageBase=pkgbase, Priority=5 + ) assert bool(package_comaintainer) assert package_comaintainer.User == user assert package_comaintainer.PackageBase == pkgbase @@ -50,7 +55,6 @@ def test_package_comaintainer_null_pkgbase_raises(user: User): PackageComaintainer(User=user, Priority=1) -def test_package_comaintainer_null_priority_raises(user: User, - pkgbase: PackageBase): +def test_package_comaintainer_null_priority_raises(user: User, pkgbase: PackageBase): with pytest.raises(IntegrityError): PackageComaintainer(User=user, PackageBase=pkgbase) diff --git a/test/test_package_comment.py b/test/test_package_comment.py index c89e23af..74f2895d 100644 --- a/test/test_package_comment.py +++ b/test/test_package_comment.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -17,9 +16,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @@ -32,35 +36,46 @@ def pkgbase(user: User) -> PackageBase: def test_package_comment_creation(user: User, pkgbase: PackageBase): with db.begin(): - package_comment = db.create(PackageComment, PackageBase=pkgbase, - User=user, Comments="Test comment.", - RenderedComment="Test rendered comment.") + package_comment = db.create( + PackageComment, + PackageBase=pkgbase, + User=user, + Comments="Test comment.", + RenderedComment="Test rendered comment.", + ) assert bool(package_comment.ID) def test_package_comment_null_pkgbase_raises(user: User): with pytest.raises(IntegrityError): - PackageComment(User=user, Comments="Test comment.", - RenderedComment="Test rendered comment.") + PackageComment( + User=user, + Comments="Test comment.", + RenderedComment="Test rendered comment.", + ) def test_package_comment_null_user_raises(pkgbase: PackageBase): with pytest.raises(IntegrityError): - PackageComment(PackageBase=pkgbase, - Comments="Test comment.", - RenderedComment="Test rendered comment.") + PackageComment( + PackageBase=pkgbase, + Comments="Test comment.", + RenderedComment="Test rendered comment.", + ) -def test_package_comment_null_comments_raises(user: User, - pkgbase: PackageBase): +def test_package_comment_null_comments_raises(user: User, pkgbase: PackageBase): with pytest.raises(IntegrityError): - PackageComment(PackageBase=pkgbase, User=user, - RenderedComment="Test rendered comment.") + PackageComment( + PackageBase=pkgbase, User=user, RenderedComment="Test rendered comment." + ) -def test_package_comment_null_renderedcomment_defaults(user: User, - pkgbase: PackageBase): +def test_package_comment_null_renderedcomment_defaults( + user: User, pkgbase: PackageBase +): with db.begin(): - record = db.create(PackageComment, PackageBase=pkgbase, - User=user, Comments="Test comment.") + record = db.create( + PackageComment, PackageBase=pkgbase, User=user, Comments="Test comment." + ) assert record.RenderedComment == str() diff --git a/test/test_package_dependency.py b/test/test_package_dependency.py index 2afbc1e3..1cd2d305 100644 --- a/test/test_package_dependency.py +++ b/test/test_package_dependency.py @@ -1,10 +1,10 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db from aurweb.models.account_type import USER_ID from aurweb.models.dependency_type import DEPENDS_ID +from aurweb.models.official_provider import OfficialProvider from aurweb.models.package import Package from aurweb.models.package_base import PackageBase from aurweb.models.package_dependency import PackageDependency @@ -19,9 +19,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd=str(), - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd=str(), + AccountTypeID=USER_ID, + ) yield user @@ -29,16 +34,21 @@ def user() -> User: def package(user: User) -> Package: with db.begin(): pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) - package = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, - Description="Test description.", - URL="https://test.package") + package = db.create( + Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="Test description.", + URL="https://test.package", + ) yield package def test_package_dependencies(user: User, package: Package): with db.begin(): - pkgdep = db.create(PackageDependency, Package=package, - DepTypeID=DEPENDS_ID, DepName="test-dep") + pkgdep = db.create( + PackageDependency, Package=package, DepTypeID=DEPENDS_ID, DepName="test-dep" + ) assert pkgdep.DepName == "test-dep" assert pkgdep.Package == package assert pkgdep in package.package_dependencies @@ -49,6 +59,22 @@ def test_package_dependencies(user: User, package: Package): db.create(Package, PackageBase=base, Name=pkgdep.DepName) assert pkgdep.is_package() + assert pkgdep.is_aur_package() + + # Test with OfficialProvider + with db.begin(): + pkgdep = db.create( + PackageDependency, + Package=package, + DepTypeID=DEPENDS_ID, + DepName="test-repo-pkg", + ) + db.create( + OfficialProvider, Name=pkgdep.DepName, Repo="extra", Provides=pkgdep.DepName + ) + + assert pkgdep.is_package() + assert not pkgdep.is_aur_package() def test_package_dependencies_null_package_raises(): diff --git a/test/test_package_group.py b/test/test_package_group.py index 0cb83ee2..163f693d 100644 --- a/test/test_package_group.py +++ b/test/test_package_group.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -19,9 +18,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user diff --git a/test/test_package_keyword.py b/test/test_package_keyword.py index ff466efc..b52547f9 100644 --- a/test/test_package_keyword.py +++ b/test/test_package_keyword.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -17,24 +16,27 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @pytest.fixture def pkgbase(user: User) -> PackageBase: with db.begin(): - pkgbase = db.create(PackageBase, Name="beautiful-package", - Maintainer=user) + pkgbase = db.create(PackageBase, Name="beautiful-package", Maintainer=user) yield pkgbase def test_package_keyword(pkgbase: PackageBase): with db.begin(): - pkg_keyword = db.create(PackageKeyword, PackageBase=pkgbase, - Keyword="test") + pkg_keyword = db.create(PackageKeyword, PackageBase=pkgbase, Keyword="test") assert pkg_keyword in pkgbase.keywords assert pkgbase == pkg_keyword.PackageBase diff --git a/test/test_package_license.py b/test/test_package_license.py index c43423b8..b9242647 100644 --- a/test/test_package_license.py +++ b/test/test_package_license.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -19,9 +18,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @@ -42,8 +46,7 @@ def package(user: User, license: License): def test_package_license(license: License, package: Package): with db.begin(): - package_license = db.create(PackageLicense, Package=package, - License=license) + package_license = db.create(PackageLicense, Package=package, License=license) assert package_license.License == license assert package_license.Package == package diff --git a/test/test_trusted_user_routes.py b/test/test_package_maintainer_routes.py similarity index 52% rename from test/test_trusted_user_routes.py rename to test/test_package_maintainer_routes.py index a5c4c5e8..1824556b 100644 --- a/test/test_trusted_user_routes.py +++ b/test/test_package_maintainer_routes.py @@ -1,23 +1,21 @@ import re - from http import HTTPStatus from io import StringIO from typing import Tuple import lxml.etree import pytest - from fastapi.testclient import TestClient from aurweb import config, db, filters, time -from aurweb.models.account_type import DEVELOPER_ID, TRUSTED_USER_ID, AccountType -from aurweb.models.tu_vote import TUVote -from aurweb.models.tu_voteinfo import TUVoteInfo +from aurweb.models.account_type import DEVELOPER_ID, PACKAGE_MAINTAINER_ID, AccountType from aurweb.models.user import User +from aurweb.models.vote import Vote +from aurweb.models.voteinfo import VoteInfo from aurweb.testing.requests import Request -DATETIME_REGEX = r'^[0-9]{4}-[0-9]{2}-[0-9]{2} \(.+\)$' -PARTICIPATION_REGEX = r'^1?[0-9]{2}[%]$' # 0% - 100% +DATETIME_REGEX = r"^[0-9]{4}-[0-9]{2}-[0-9]{2} \(.+\)$" +PARTICIPATION_REGEX = r"^1?[0-9]{2}[%]$" # 0% - 100% def parse_root(html): @@ -43,11 +41,11 @@ def get_pkglist_directions(table): def get_a(node): - return node.xpath('./a')[0].text.strip() + return node.xpath("./a")[0].text.strip() def get_span(node): - return node.xpath('./span')[0].text.strip() + return node.xpath("./span")[0].text.strip() def assert_current_vote_html(row, expected): @@ -82,83 +80,108 @@ def setup(db_test): @pytest.fixture def client(): from aurweb.asgi import app - yield TestClient(app=app) + + client = TestClient(app=app) + + # disable redirects for our tests + client.follow_redirects = False + yield client @pytest.fixture -def tu_user(): - tu_type = db.query(AccountType, - AccountType.AccountType == "Trusted User").first() +def pm_user(): + pm_type = db.query( + AccountType, AccountType.AccountType == "Package Maintainer" + ).first() with db.begin(): - tu_user = db.create(User, Username="test_tu", - Email="test_tu@example.org", - RealName="Test TU", Passwd="testPassword", - AccountType=tu_type) - yield tu_user + pm_user = db.create( + User, + Username="test_pm", + Email="test_pm@example.org", + RealName="Test PM", + Passwd="testPassword", + AccountType=pm_type, + ) + yield pm_user @pytest.fixture -def tu_user2(): +def pm_user2(): with db.begin(): - tu_user2 = db.create(User, Username="test_tu2", - Email="test_tu2@example.org", - RealName="Test TU 2", Passwd="testPassword", - AccountTypeID=TRUSTED_USER_ID) - yield tu_user2 + pm_user2 = db.create( + User, + Username="test_pm2", + Email="test_pm2@example.org", + RealName="Test PM 2", + Passwd="testPassword", + AccountTypeID=PACKAGE_MAINTAINER_ID, + ) + yield pm_user2 @pytest.fixture def user(): - user_type = db.query(AccountType, - AccountType.AccountType == "User").first() + user_type = db.query(AccountType, AccountType.AccountType == "User").first() with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountType=user_type) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountType=user_type, + ) yield user @pytest.fixture -def proposal(user, tu_user): +def proposal(user, pm_user): ts = time.utcnow() agenda = "Test proposal." start = ts - 5 end = ts + 1000 with db.begin(): - voteinfo = db.create(TUVoteInfo, - Agenda=agenda, Quorum=0.0, - User=user.Username, Submitter=tu_user, - Submitted=start, End=end) - yield (tu_user, user, voteinfo) + voteinfo = db.create( + VoteInfo, + Agenda=agenda, + Quorum=0.0, + User=user.Username, + Submitter=pm_user, + Submitted=start, + End=end, + ) + yield (pm_user, user, voteinfo) -def test_tu_index_guest(client): - headers = {"referer": config.get("options", "aur_location") + "/tu"} +def test_pm_index_guest(client): + headers = {"referer": config.get("options", "aur_location") + "/package-maintainer"} with client as request: - response = request.get("/tu", allow_redirects=False, headers=headers) + response = request.get("/package-maintainer", headers=headers) assert response.status_code == int(HTTPStatus.SEE_OTHER) - params = filters.urlencode({"next": "/tu"}) + params = filters.urlencode({"next": "/package-maintainer"}) assert response.headers.get("location") == f"/login?{params}" -def test_tu_index_unauthorized(client: TestClient, user: User): +def test_pm_index_unauthorized(client: TestClient, user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: # Login as a normal user, not a TU. - response = request.get("/tu", cookies=cookies, allow_redirects=False) + request.cookies = cookies + response = request.get("/package-maintainer") assert response.status_code == int(HTTPStatus.SEE_OTHER) assert response.headers.get("location") == "/" -def test_tu_empty_index(client, tu_user): - """ Check an empty index when we don't create any records. """ +def test_pm_empty_index(client, pm_user): + """Check an empty index when we don't create any records.""" - # Make a default get request to /tu. - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + # Make a default get request to /package-maintainer. + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: - response = request.get("/tu", cookies=cookies, allow_redirects=False) + request.cookies = cookies + response = request.get("/package-maintainer") assert response.status_code == int(HTTPStatus.OK) # Parse lxml root. @@ -173,50 +196,56 @@ def test_tu_empty_index(client, tu_user): assert len(tables) == 0 -def test_tu_index(client, tu_user): +def test_pm_index(client, pm_user): ts = time.utcnow() # Create some test votes: (Agenda, Start, End). votes = [ ("Test agenda 1", ts - 5, ts + 1000), # Still running. - ("Test agenda 2", ts - 1000, ts - 5) # Not running anymore. + ("Test agenda 2", ts - 1000, ts - 5), # Not running anymore. ] vote_records = [] with db.begin(): for vote in votes: agenda, start, end = vote vote_records.append( - db.create(TUVoteInfo, Agenda=agenda, - User=tu_user.Username, - Submitted=start, End=end, - Quorum=0.0, - Submitter=tu_user)) + db.create( + VoteInfo, + Agenda=agenda, + User=pm_user.Username, + Submitted=start, + End=end, + Quorum=0.0, + Submitter=pm_user, + ) + ) with db.begin(): # Vote on an ended proposal. vote_record = vote_records[1] vote_record.Yes += 1 - vote_record.ActiveTUs += 1 - db.create(TUVote, VoteInfo=vote_record, User=tu_user) + vote_record.ActiveUsers += 1 + db.create(Vote, VoteInfo=vote_record, User=pm_user) - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: # Pass an invalid cby and pby; let them default to "desc". - response = request.get("/tu", cookies=cookies, params={ - "cby": "BAD!", - "pby": "blah" - }, allow_redirects=False) + request.cookies = cookies + response = request.get( + "/package-maintainer", + params={"cby": "BAD!", "pby": "blah"}, + ) assert response.status_code == int(HTTPStatus.OK) - # Rows we expect to exist in HTML produced by /tu for current votes. + # Rows we expect to exist in HTML produced by /package-maintainer for current votes. expected_rows = [ ( - r'Test agenda 1', + r"Test agenda 1", DATETIME_REGEX, DATETIME_REGEX, - tu_user.Username, - r'^(Yes|No)$' + pm_user.Username, + r"^(Yes|No)$", ) ] @@ -236,16 +265,16 @@ def test_tu_index(client, tu_user): past_votes = [c for c in votes if c[2] <= ts] assert len(past_votes) == len(expected_rows) - # Rows we expect to exist in HTML produced by /tu for past votes. + # Rows we expect to exist in HTML produced by /package-maintainer for past votes. expected_rows = [ ( - r'Test agenda 2', + r"Test agenda 2", DATETIME_REGEX, DATETIME_REGEX, - tu_user.Username, - r'^\d+$', - r'^\d+$', - r'^(Yes|No)$' + pm_user.Username, + r"^\d+$", + r"^\d+$", + r"^(Yes|No)$", ) ] @@ -263,33 +292,86 @@ def test_tu_index(client, tu_user): username, vote_id = rows[0] username = username.xpath("./a")[0] vote_id = vote_id.xpath("./a")[0] - assert username.text.strip() == tu_user.Username + assert username.text.strip() == pm_user.Username assert int(vote_id.text.strip()) == vote_records[1].ID -def test_tu_index_table_paging(client, tu_user): +def test_pm_stats(client: TestClient, pm_user: User): + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} + with client as request: + request.cookies = cookies + response = request.get("/package-maintainer") + assert response.status_code == HTTPStatus.OK + + root = parse_root(response.text) + stats = root.xpath('//table[@class="no-width"]')[0] + rows = stats.xpath("./tbody/tr") + + # We have one package maintainer. + total = rows[0] + label, count = total.xpath("./td") + assert int(count.text.strip()) == 1 + + # And we have one active PM. + active = rows[1] + label, count = active.xpath("./td") + assert int(count.text.strip()) == 1 + + with db.begin(): + pm_user.InactivityTS = time.utcnow() + + with client as request: + request.cookies = cookies + response = request.get("/package-maintainer") + assert response.status_code == HTTPStatus.OK + + root = parse_root(response.text) + stats = root.xpath('//table[@class="no-width"]')[0] + rows = stats.xpath("./tbody/tr") + + # We have one package maintainer. + total = rows[0] + label, count = total.xpath("./td") + assert int(count.text.strip()) == 1 + + # But we have no more active PMs. + active = rows[1] + label, count = active.xpath("./td") + assert int(count.text.strip()) == 0 + + +def test_pm_index_table_paging(client, pm_user): ts = time.utcnow() with db.begin(): for i in range(25): # Create 25 current votes. - db.create(TUVoteInfo, Agenda=f"Agenda #{i}", - User=tu_user.Username, - Submitted=(ts - 5), End=(ts + 1000), - Quorum=0.0, - Submitter=tu_user) + db.create( + VoteInfo, + Agenda=f"Agenda #{i}", + User=pm_user.Username, + Submitted=(ts - 5), + End=(ts + 1000), + Quorum=0.0, + Submitter=pm_user, + ) for i in range(25): # Create 25 past votes. - db.create(TUVoteInfo, Agenda=f"Agenda #{25 + i}", - User=tu_user.Username, - Submitted=(ts - 1000), End=(ts - 5), - Quorum=0.0, - Submitter=tu_user) + db.create( + VoteInfo, + Agenda=f"Agenda #{25 + i}", + User=pm_user.Username, + Submitted=(ts - 1000), + End=(ts - 5), + Quorum=0.0, + Submitter=pm_user, + ) - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: - response = request.get("/tu", cookies=cookies, allow_redirects=False) + request.cookies = cookies + response = request.get("/package-maintainer") assert response.status_code == int(HTTPStatus.OK) # Parse lxml.etree root. @@ -304,8 +386,8 @@ def test_tu_index_table_paging(client, tu_user): f"Agenda #{offset + i}", DATETIME_REGEX, DATETIME_REGEX, - tu_user.Username, - r'^(Yes|No)$' + pm_user.Username, + r"^(Yes|No)$", ] for i, row in enumerate(rows): @@ -319,9 +401,8 @@ def test_tu_index_table_paging(client, tu_user): # Now, get the next page of current votes. offset = 10 # Specify coff=10 with client as request: - response = request.get("/tu", cookies=cookies, params={ - "coff": offset - }, allow_redirects=False) + request.cookies = cookies + response = request.get("package-maintainer", params={"coff": offset}) assert response.status_code == int(HTTPStatus.OK) old_rows = rows @@ -348,9 +429,8 @@ def test_tu_index_table_paging(client, tu_user): offset = 20 # Specify coff=10 with client as request: - response = request.get("/tu", cookies=cookies, params={ - "coff": offset - }, allow_redirects=False) + request.cookies = cookies + response = request.get("/package-maintainer", params={"coff": offset}) assert response.status_code == int(HTTPStatus.OK) # Do it again, we only have five left. @@ -375,27 +455,32 @@ def test_tu_index_table_paging(client, tu_user): assert "Next" in past_directions[0].text -def test_tu_index_sorting(client, tu_user): +def test_pm_index_sorting(client, pm_user): ts = time.utcnow() with db.begin(): for i in range(2): # Create 'Agenda #1' and 'Agenda #2'. - db.create(TUVoteInfo, Agenda=f"Agenda #{i + 1}", - User=tu_user.Username, - Submitted=(ts + 5), End=(ts + 1000), - Quorum=0.0, - Submitter=tu_user) + db.create( + VoteInfo, + Agenda=f"Agenda #{i + 1}", + User=pm_user.Username, + Submitted=(ts + 5), + End=(ts + 1000), + Quorum=0.0, + Submitter=pm_user, + ) # Let's order each vote one day after the other. # This will allow us to test the sorting nature # of the tables. ts += 86405 - # Make a default request to /tu. - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + # Make a default request to /package-maintainer. + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: - response = request.get("/tu", cookies=cookies, allow_redirects=False) + request.cookies = cookies + response = request.get("/package-maintainer") assert response.status_code == int(HTTPStatus.OK) # Get lxml handles of the document. @@ -404,27 +489,26 @@ def test_tu_index_sorting(client, tu_user): rows = get_table_rows(table) # The latest Agenda is at the top by default. - expected = [ - "Agenda #2", - "Agenda #1" - ] + expected = ["Agenda #2", "Agenda #1"] assert len(rows) == len(expected) for i, row in enumerate(rows): - assert_current_vote_html(row, [ - expected[i], - DATETIME_REGEX, - DATETIME_REGEX, - tu_user.Username, - r'^(Yes|No)$' - ]) + assert_current_vote_html( + row, + [ + expected[i], + DATETIME_REGEX, + DATETIME_REGEX, + pm_user.Username, + r"^(Yes|No)$", + ], + ) # Make another request; one that sorts the current votes # in ascending order instead of the default descending order. with client as request: - response = request.get("/tu", cookies=cookies, params={ - "cby": "asc" - }, allow_redirects=False) + request.cookies = cookies + response = request.get("/package-maintainer", params={"cby": "asc"}) assert response.status_code == int(HTTPStatus.OK) # Get lxml handles of the document. @@ -436,39 +520,47 @@ def test_tu_index_sorting(client, tu_user): rev_expected = list(reversed(expected)) assert len(rows) == len(rev_expected) for i, row in enumerate(rows): - assert_current_vote_html(row, [ - rev_expected[i], - DATETIME_REGEX, - DATETIME_REGEX, - tu_user.Username, - r'^(Yes|No)$' - ]) + assert_current_vote_html( + row, + [ + rev_expected[i], + DATETIME_REGEX, + DATETIME_REGEX, + pm_user.Username, + r"^(Yes|No)$", + ], + ) -def test_tu_index_last_votes(client: TestClient, tu_user: User, tu_user2: User, - user: User): +def test_pm_index_last_votes( + client: TestClient, pm_user: User, pm_user2: User, user: User +): ts = time.utcnow() with db.begin(): # Create a proposal which has ended. - voteinfo = db.create(TUVoteInfo, Agenda="Test agenda", - User=user.Username, - Submitted=(ts - 1000), - End=(ts - 5), - Yes=1, - No=1, - ActiveTUs=1, - Quorum=0.0, - Submitter=tu_user) + voteinfo = db.create( + VoteInfo, + Agenda="Test agenda", + User=user.Username, + Submitted=(ts - 1000), + End=(ts - 5), + Yes=1, + No=1, + ActiveUsers=1, + Quorum=0.0, + Submitter=pm_user, + ) - # Create a vote on it from tu_user. - db.create(TUVote, VoteInfo=voteinfo, User=tu_user) - db.create(TUVote, VoteInfo=voteinfo, User=tu_user2) + # Create a vote on it from pm_user. + db.create(Vote, VoteInfo=voteinfo, User=pm_user) + db.create(Vote, VoteInfo=voteinfo, User=pm_user2) - # Now, check that tu_user got populated in the .last-votes table. - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + # Now, check that pm_user got populated in the .last-votes table. + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: - response = request.get("/tu", cookies=cookies) + request.cookies = cookies + response = request.get("/package-maintainer") assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -478,49 +570,54 @@ def test_tu_index_last_votes(client: TestClient, tu_user: User, tu_user2: User, last_vote = rows[0] user, vote_id = last_vote.xpath("./td/a") - assert user.text.strip() == tu_user.Username + assert user.text.strip() == pm_user.Username assert int(vote_id.text.strip()) == voteinfo.ID last_vote = rows[1] user, vote_id = last_vote.xpath("./td/a") assert int(vote_id.text.strip()) == voteinfo.ID - assert user.text.strip() == tu_user2.Username + assert user.text.strip() == pm_user2.Username -def test_tu_proposal_not_found(client, tu_user): - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} +def test_pm_proposal_not_found(client, pm_user): + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: - response = request.get("/tu", params={"id": 1}, cookies=cookies) + request.cookies = cookies + response = request.get( + "/package-maintainer", params={"id": 1}, follow_redirects=True + ) assert response.status_code == int(HTTPStatus.NOT_FOUND) -def test_tu_proposal_unauthorized(client: TestClient, user: User, - proposal: Tuple[User, User, TUVoteInfo]): +def test_pm_proposal_unauthorized( + client: TestClient, user: User, proposal: Tuple[User, User, VoteInfo] +): cookies = {"AURSID": user.login(Request(), "testPassword")} - endpoint = f"/tu/{proposal[2].ID}" + endpoint = f"/package-maintainer/{proposal[2].ID}" with client as request: - response = request.get(endpoint, cookies=cookies, - allow_redirects=False) + request.cookies = cookies + response = request.get(endpoint) assert response.status_code == int(HTTPStatus.SEE_OTHER) - assert response.headers.get("location") == "/tu" + assert response.headers.get("location") == "/package-maintainer" with client as request: - response = request.post(endpoint, cookies=cookies, - data={"decision": False}, - allow_redirects=False) + request.cookies = cookies + response = request.post(endpoint, data={"decision": False}) assert response.status_code == int(HTTPStatus.SEE_OTHER) - assert response.headers.get("location") == "/tu" + assert response.headers.get("location") == "/package-maintainer" -def test_tu_running_proposal(client: TestClient, - proposal: Tuple[User, User, TUVoteInfo]): - tu_user, user, voteinfo = proposal +def test_pm_running_proposal(client: TestClient, proposal: Tuple[User, User, VoteInfo]): + pm_user, user, voteinfo = proposal + with db.begin(): + voteinfo.ActiveUsers = 1 - # Initiate an authenticated GET request to /tu/{proposal_id}. + # Initiate an authenticated GET request to /package-maintainer/{proposal_id}. proposal_id = voteinfo.ID - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: - response = request.get(f"/tu/{proposal_id}", cookies=cookies) + request.cookies = cookies + response = request.get(f"/package-maintainer/{proposal_id}") assert response.status_code == int(HTTPStatus.OK) # Alright, now let's continue on to verifying some markup. @@ -532,25 +629,34 @@ def test_tu_running_proposal(client: TestClient, assert vote_running.text.strip() == "This vote is still running." # Verify User field. - username = details.xpath( - './div[contains(@class, "user")]/strong/a/text()')[0] + username = details.xpath('./div[contains(@class, "user")]/strong/a/text()')[0] assert username.strip() == user.Username - submitted = details.xpath( - './div[contains(@class, "submitted")]/text()')[0] - assert re.match(r'^Submitted: \d{4}-\d{2}-\d{2} \d{2}:\d{2} \(.+\) by$', - submitted.strip()) is not None + active = details.xpath('./div[contains(@class, "field")]')[1] + content = active.text.strip() + assert "Active Package Maintainers assigned:" in content + assert "1" in content + + submitted = details.xpath('./div[contains(@class, "submitted")]/text()')[0] + assert ( + re.match( + r"^Submitted: \d{4}-\d{2}-\d{2} \d{2}:\d{2} \(.+\) by$", submitted.strip() + ) + is not None + ) submitter = details.xpath('./div[contains(@class, "submitted")]/a')[0] - assert submitter.text.strip() == tu_user.Username - assert submitter.attrib["href"] == f"/account/{tu_user.Username}" + assert submitter.text.strip() == pm_user.Username + assert submitter.attrib["href"] == f"/account/{pm_user.Username}" end = details.xpath('./div[contains(@class, "end")]')[0] end_label = end.xpath("./text()")[0] assert end_label.strip() == "End:" end_datetime = end.xpath("./strong/text()")[0] - assert re.match(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2} \(.+\)$', - end_datetime.strip()) is not None + assert ( + re.match(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2} \(.+\)$", end_datetime.strip()) + is not None + ) # We have not voted yet. Assert that our voting form is shown. form = root.xpath('//form[contains(@class, "action-form")]')[0] @@ -575,14 +681,16 @@ def test_tu_running_proposal(client: TestClient, # Create a vote. with db.begin(): - db.create(TUVote, VoteInfo=voteinfo, User=tu_user) - voteinfo.ActiveTUs += 1 + db.create(Vote, VoteInfo=voteinfo, User=pm_user) + voteinfo.ActiveUsers += 1 voteinfo.Yes += 1 # Make another request now that we've voted. with client as request: + request.cookies = cookies response = request.get( - "/tu", params={"id": voteinfo.ID}, cookies=cookies) + "/package-maintainer", params={"id": voteinfo.ID}, follow_redirects=True + ) assert response.status_code == int(HTTPStatus.OK) # Parse our new root. @@ -597,18 +705,19 @@ def test_tu_running_proposal(client: TestClient, assert status == "You've already voted for this proposal." -def test_tu_ended_proposal(client, proposal): - tu_user, user, voteinfo = proposal +def test_pm_ended_proposal(client, proposal): + pm_user, user, voteinfo = proposal ts = time.utcnow() with db.begin(): voteinfo.End = ts - 5 # 5 seconds ago. - # Initiate an authenticated GET request to /tu/{proposal_id}. + # Initiate an authenticated GET request to /package-maintainer/{proposal_id}. proposal_id = voteinfo.ID - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: - response = request.get(f"/tu/{proposal_id}", cookies=cookies) + request.cookies = cookies + response = request.get(f"/package-maintainer/{proposal_id}") assert response.status_code == int(HTTPStatus.OK) # Alright, now let's continue on to verifying some markup. @@ -635,35 +744,34 @@ def test_tu_ended_proposal(client, proposal): assert status == "Voting is closed for this proposal." -def test_tu_proposal_vote_not_found(client, tu_user): - """ Test POST request to a missing vote. """ - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} +def test_pm_proposal_vote_not_found(client, pm_user): + """Test POST request to a missing vote.""" + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: data = {"decision": "Yes"} - response = request.post("/tu/1", cookies=cookies, - data=data, allow_redirects=False) + request.cookies = cookies + response = request.post("/package-maintainer/1", data=data) assert response.status_code == int(HTTPStatus.NOT_FOUND) -def test_tu_proposal_vote(client, proposal): - tu_user, user, voteinfo = proposal +def test_pm_proposal_vote(client, proposal): + pm_user, user, voteinfo = proposal # Store the current related values. yes = voteinfo.Yes - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: data = {"decision": "Yes"} - response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, - data=data) + request.cookies = cookies + response = request.post(f"/package-maintainer/{voteinfo.ID}", data=data) assert response.status_code == int(HTTPStatus.OK) # Check that the proposal record got updated. assert voteinfo.Yes == yes + 1 - # Check that the new TUVote exists. - vote = db.query(TUVote, TUVote.VoteInfo == voteinfo, - TUVote.User == tu_user).first() + # Check that the new PMVote exists. + vote = db.query(Vote, Vote.VoteInfo == voteinfo, Vote.User == pm_user).first() assert vote is not None root = parse_root(response.text) @@ -673,47 +781,48 @@ def test_tu_proposal_vote(client, proposal): assert status == "You've already voted for this proposal." -def test_tu_proposal_vote_unauthorized( - client: TestClient, proposal: Tuple[User, User, TUVoteInfo]): - tu_user, user, voteinfo = proposal +def test_pm_proposal_vote_unauthorized( + client: TestClient, proposal: Tuple[User, User, VoteInfo] +): + pm_user, user, voteinfo = proposal with db.begin(): - tu_user.AccountTypeID = DEVELOPER_ID + pm_user.AccountTypeID = DEVELOPER_ID - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: data = {"decision": "Yes"} - response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, - data=data, allow_redirects=False) + request.cookies = cookies + response = request.post(f"package-maintainer/{voteinfo.ID}", data=data) assert response.status_code == int(HTTPStatus.UNAUTHORIZED) root = parse_root(response.text) status = root.xpath('//span[contains(@class, "status")]/text()')[0] - assert status == "Only Trusted Users are allowed to vote." + assert status == "Only Package Maintainers are allowed to vote." with client as request: data = {"decision": "Yes"} - response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies, - data=data, allow_redirects=False) + request.cookies = cookies + response = request.get(f"/package-maintainer/{voteinfo.ID}", params=data) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) status = root.xpath('//span[contains(@class, "status")]/text()')[0] - assert status == "Only Trusted Users are allowed to vote." + assert status == "Only Package Maintainers are allowed to vote." -def test_tu_proposal_vote_cant_self_vote(client, proposal): - tu_user, user, voteinfo = proposal +def test_pm_proposal_vote_cant_self_vote(client, proposal): + pm_user, user, voteinfo = proposal # Update voteinfo.User. with db.begin(): - voteinfo.User = tu_user.Username + voteinfo.User = pm_user.Username - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: data = {"decision": "Yes"} - response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, - data=data, allow_redirects=False) + request.cookies = cookies + response = request.post(f"/package-maintainer/{voteinfo.ID}", data=data) assert response.status_code == int(HTTPStatus.BAD_REQUEST) root = parse_root(response.text) @@ -722,8 +831,8 @@ def test_tu_proposal_vote_cant_self_vote(client, proposal): with client as request: data = {"decision": "Yes"} - response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies, - data=data, allow_redirects=False) + request.cookies = cookies + response = request.get(f"/package-maintainer/{voteinfo.ID}", params=data) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -731,19 +840,19 @@ def test_tu_proposal_vote_cant_self_vote(client, proposal): assert status == "You cannot vote in an proposal about you." -def test_tu_proposal_vote_already_voted(client, proposal): - tu_user, user, voteinfo = proposal +def test_pm_proposal_vote_already_voted(client, proposal): + pm_user, user, voteinfo = proposal with db.begin(): - db.create(TUVote, VoteInfo=voteinfo, User=tu_user) + db.create(Vote, VoteInfo=voteinfo, User=pm_user) voteinfo.Yes += 1 - voteinfo.ActiveTUs += 1 + voteinfo.ActiveUsers += 1 - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: data = {"decision": "Yes"} - response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, - data=data, allow_redirects=False) + request.cookies = cookies + response = request.post(f"/package-maintainer/{voteinfo.ID}", data=data) assert response.status_code == int(HTTPStatus.BAD_REQUEST) root = parse_root(response.text) @@ -752,8 +861,8 @@ def test_tu_proposal_vote_already_voted(client, proposal): with client as request: data = {"decision": "Yes"} - response = request.get(f"/tu/{voteinfo.ID}", cookies=cookies, - data=data, allow_redirects=False) + request.cookies = cookies + response = request.get(f"/package-maintainer/{voteinfo.ID}", params=data) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -761,46 +870,48 @@ def test_tu_proposal_vote_already_voted(client, proposal): assert status == "You've already voted for this proposal." -def test_tu_proposal_vote_invalid_decision(client, proposal): - tu_user, user, voteinfo = proposal +def test_pm_proposal_vote_invalid_decision(client, proposal): + pm_user, user, voteinfo = proposal - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: data = {"decision": "EVIL"} - response = request.post(f"/tu/{voteinfo.ID}", cookies=cookies, - data=data) + request.cookies = cookies + response = request.post(f"package-maintainer/{voteinfo.ID}", data=data) assert response.status_code == int(HTTPStatus.BAD_REQUEST) assert response.text == "Invalid 'decision' value." -def test_tu_addvote(client: TestClient, tu_user: User): - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} +def test_pm_addvote(client: TestClient, pm_user: User): + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: - response = request.get("/addvote", cookies=cookies) + request.cookies = cookies + response = request.get("/addvote") assert response.status_code == int(HTTPStatus.OK) -def test_tu_addvote_unauthorized(client: TestClient, user: User, - proposal: Tuple[User, User, TUVoteInfo]): +def test_pm_addvote_unauthorized( + client: TestClient, user: User, proposal: Tuple[User, User, VoteInfo] +): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - response = request.get("/addvote", cookies=cookies, - allow_redirects=False) + request.cookies = cookies + response = request.get("/addvote") assert response.status_code == int(HTTPStatus.SEE_OTHER) - assert response.headers.get("location") == "/tu" + assert response.headers.get("location") == "/package-maintainer" with client as request: - response = request.post("/addvote", cookies=cookies, - allow_redirects=False) + request.cookies = cookies + response = request.post("/addvote") assert response.status_code == int(HTTPStatus.SEE_OTHER) - assert response.headers.get("location") == "/tu" + assert response.headers.get("location") == "/package-maintainer" -def test_tu_addvote_invalid_type(client: TestClient, tu_user: User): - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} +def test_pm_addvote_invalid_type(client: TestClient, pm_user: User): + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: - response = request.get("/addvote", params={"type": "faketype"}, - cookies=cookies) + request.cookies = cookies + response = request.get("/addvote", params={"type": "faketype"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -808,75 +919,73 @@ def test_tu_addvote_invalid_type(client: TestClient, tu_user: User): assert error.strip() == "Invalid type." -def test_tu_addvote_post(client: TestClient, tu_user: User, user: User): - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} +def test_pm_addvote_post(client: TestClient, pm_user: User, user: User): + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} - data = { - "user": user.Username, - "type": "add_tu", - "agenda": "Blah" - } + data = {"user": user.Username, "type": "add_pm", "agenda": "Blah"} with client as request: - response = request.post("/addvote", cookies=cookies, data=data) + request.cookies = cookies + response = request.post("/addvote", data=data) assert response.status_code == int(HTTPStatus.SEE_OTHER) - voteinfo = db.query(TUVoteInfo, TUVoteInfo.Agenda == "Blah").first() + voteinfo = db.query(VoteInfo, VoteInfo.Agenda == "Blah").first() assert voteinfo is not None -def test_tu_addvote_post_cant_duplicate_username(client: TestClient, - tu_user: User, user: User): - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} +def test_pm_addvote_post_cant_duplicate_username( + client: TestClient, pm_user: User, user: User +): + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} - data = { - "user": user.Username, - "type": "add_tu", - "agenda": "Blah" - } + data = {"user": user.Username, "type": "add_pm", "agenda": "Blah"} with client as request: - response = request.post("/addvote", cookies=cookies, data=data) + request.cookies = cookies + response = request.post("/addvote", data=data) assert response.status_code == int(HTTPStatus.SEE_OTHER) - voteinfo = db.query(TUVoteInfo, TUVoteInfo.Agenda == "Blah").first() + voteinfo = db.query(VoteInfo, VoteInfo.Agenda == "Blah").first() assert voteinfo is not None with client as request: - response = request.post("/addvote", cookies=cookies, data=data) + request.cookies = cookies + response = request.post("/addvote", data=data) assert response.status_code == int(HTTPStatus.BAD_REQUEST) -def test_tu_addvote_post_invalid_username(client: TestClient, tu_user: User): - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} +def test_pm_addvote_post_invalid_username(client: TestClient, pm_user: User): + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} data = {"user": "fakeusername"} with client as request: - response = request.post("/addvote", cookies=cookies, data=data) + request.cookies = cookies + response = request.post("/addvote", data=data) assert response.status_code == int(HTTPStatus.NOT_FOUND) -def test_tu_addvote_post_invalid_type(client: TestClient, tu_user: User, - user: User): - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} +def test_pm_addvote_post_invalid_type(client: TestClient, pm_user: User, user: User): + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} data = {"user": user.Username} with client as request: - response = request.post("/addvote", cookies=cookies, data=data) + request.cookies = cookies + response = request.post("/addvote", data=data) assert response.status_code == int(HTTPStatus.BAD_REQUEST) -def test_tu_addvote_post_invalid_agenda(client: TestClient, - tu_user: User, user: User): - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} - data = {"user": user.Username, "type": "add_tu"} +def test_pm_addvote_post_invalid_agenda(client: TestClient, pm_user: User, user: User): + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} + data = {"user": user.Username, "type": "add_pm"} with client as request: - response = request.post("/addvote", cookies=cookies, data=data) + request.cookies = cookies + response = request.post("/addvote", data=data) assert response.status_code == int(HTTPStatus.BAD_REQUEST) -def test_tu_addvote_post_bylaws(client: TestClient, tu_user: User): +def test_pm_addvote_post_bylaws(client: TestClient, pm_user: User): # Bylaws votes do not need a user specified. - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} data = {"type": "bylaws", "agenda": "Blah blah!"} with client as request: - response = request.post("/addvote", cookies=cookies, data=data) + request.cookies = cookies + response = request.post("/addvote", data=data) assert response.status_code == int(HTTPStatus.SEE_OTHER) diff --git a/test/test_package_notification.py b/test/test_package_notification.py index e7a72a43..27a03e84 100644 --- a/test/test_package_notification.py +++ b/test/test_package_notification.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -16,8 +15,13 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword") + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + ) yield user @@ -31,7 +35,8 @@ def pkgbase(user: User) -> PackageBase: def test_package_notification_creation(user: User, pkgbase: PackageBase): with db.begin(): package_notification = db.create( - PackageNotification, User=user, PackageBase=pkgbase) + PackageNotification, User=user, PackageBase=pkgbase + ) assert bool(package_notification) assert package_notification.User == user assert package_notification.PackageBase == pkgbase diff --git a/test/test_package_relation.py b/test/test_package_relation.py index 6e9a5545..c20b1394 100644 --- a/test/test_package_relation.py +++ b/test/test_package_relation.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -19,9 +18,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @@ -29,17 +33,24 @@ def user() -> User: def package(user: User) -> Package: with db.begin(): pkgbase = db.create(PackageBase, Name="test-package", Maintainer=user) - package = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, - Description="Test description.", - URL="https://test.package") + package = db.create( + Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="Test description.", + URL="https://test.package", + ) yield package def test_package_relation(package: Package): with db.begin(): - pkgrel = db.create(PackageRelation, Package=package, - RelTypeID=CONFLICTS_ID, - RelName="test-relation") + pkgrel = db.create( + PackageRelation, + Package=package, + RelTypeID=CONFLICTS_ID, + RelName="test-relation", + ) assert pkgrel.RelName == "test-relation" assert pkgrel.Package == package diff --git a/test/test_package_request.py b/test/test_package_request.py index 3474c565..2bbf56c2 100644 --- a/test/test_package_request.py +++ b/test/test_package_request.py @@ -1,12 +1,20 @@ import pytest - from sqlalchemy.exc import IntegrityError -from aurweb import db, time +from aurweb import config, db, time from aurweb.models.account_type import USER_ID from aurweb.models.package_base import PackageBase -from aurweb.models.package_request import (ACCEPTED, ACCEPTED_ID, CLOSED, CLOSED_ID, PENDING, PENDING_ID, REJECTED, - REJECTED_ID, PackageRequest) +from aurweb.models.package_request import ( + ACCEPTED, + ACCEPTED_ID, + CLOSED, + CLOSED_ID, + PENDING, + PENDING_ID, + REJECTED, + REJECTED_ID, + PackageRequest, +) from aurweb.models.request_type import MERGE_ID from aurweb.models.user import User @@ -19,9 +27,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @@ -34,10 +47,15 @@ def pkgbase(user: User) -> PackageBase: def test_package_request_creation(user: User, pkgbase: PackageBase): with db.begin(): - package_request = db.create(PackageRequest, ReqTypeID=MERGE_ID, - User=user, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Comments=str(), ClosureComment=str()) + package_request = db.create( + PackageRequest, + ReqTypeID=MERGE_ID, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), + ClosureComment=str(), + ) assert bool(package_request.ID) assert package_request.User == user @@ -54,11 +72,17 @@ def test_package_request_creation(user: User, pkgbase: PackageBase): def test_package_request_closed(user: User, pkgbase: PackageBase): ts = time.utcnow() with db.begin(): - package_request = db.create(PackageRequest, ReqTypeID=MERGE_ID, - User=user, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Closer=user, ClosedTS=ts, - Comments=str(), ClosureComment=str()) + package_request = db.create( + PackageRequest, + ReqTypeID=MERGE_ID, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Closer=user, + ClosedTS=ts, + Comments=str(), + ClosureComment=str(), + ) assert package_request.Closer == user assert package_request.ClosedTS == ts @@ -67,61 +91,87 @@ def test_package_request_closed(user: User, pkgbase: PackageBase): assert package_request in user.closed_requests -def test_package_request_null_request_type_raises(user: User, - pkgbase: PackageBase): +def test_package_request_null_request_type_raises(user: User, pkgbase: PackageBase): with pytest.raises(IntegrityError): - PackageRequest(User=user, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Comments=str(), ClosureComment=str()) + PackageRequest( + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), + ClosureComment=str(), + ) def test_package_request_null_user_raises(pkgbase: PackageBase): with pytest.raises(IntegrityError): - PackageRequest(ReqTypeID=MERGE_ID, - PackageBase=pkgbase, PackageBaseName=pkgbase.Name, - Comments=str(), ClosureComment=str()) + PackageRequest( + ReqTypeID=MERGE_ID, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), + ClosureComment=str(), + ) -def test_package_request_null_package_base_raises(user: User, - pkgbase: PackageBase): +def test_package_request_null_package_base_raises(user: User, pkgbase: PackageBase): with pytest.raises(IntegrityError): - PackageRequest(ReqTypeID=MERGE_ID, - User=user, PackageBaseName=pkgbase.Name, - Comments=str(), ClosureComment=str()) + PackageRequest( + ReqTypeID=MERGE_ID, + User=user, + PackageBaseName=pkgbase.Name, + Comments=str(), + ClosureComment=str(), + ) -def test_package_request_null_package_base_name_raises(user: User, - pkgbase: PackageBase): +def test_package_request_null_package_base_name_raises( + user: User, pkgbase: PackageBase +): with pytest.raises(IntegrityError): - PackageRequest(ReqTypeID=MERGE_ID, - User=user, PackageBase=pkgbase, - Comments=str(), ClosureComment=str()) + PackageRequest( + ReqTypeID=MERGE_ID, + User=user, + PackageBase=pkgbase, + Comments=str(), + ClosureComment=str(), + ) -def test_package_request_null_comments_raises(user: User, - pkgbase: PackageBase): +def test_package_request_null_comments_raises(user: User, pkgbase: PackageBase): with pytest.raises(IntegrityError): - PackageRequest(ReqTypeID=MERGE_ID, User=user, - PackageBase=pkgbase, PackageBaseName=pkgbase.Name, - ClosureComment=str()) + PackageRequest( + ReqTypeID=MERGE_ID, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + ClosureComment=str(), + ) -def test_package_request_null_closure_comment_raises(user: User, - pkgbase: PackageBase): +def test_package_request_null_closure_comment_raises(user: User, pkgbase: PackageBase): with pytest.raises(IntegrityError): - PackageRequest(ReqTypeID=MERGE_ID, User=user, - PackageBase=pkgbase, PackageBaseName=pkgbase.Name, - Comments=str()) + PackageRequest( + ReqTypeID=MERGE_ID, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), + ) def test_package_request_status_display(user: User, pkgbase: PackageBase): - """ Test status_display() based on the Status column value. """ + """Test status_display() based on the Status column value.""" with db.begin(): - pkgreq = db.create(PackageRequest, ReqTypeID=MERGE_ID, - User=user, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Comments=str(), ClosureComment=str(), - Status=PENDING_ID) + pkgreq = db.create( + PackageRequest, + ReqTypeID=MERGE_ID, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), + ClosureComment=str(), + Status=PENDING_ID, + ) assert pkgreq.status_display() == PENDING with db.begin(): @@ -140,3 +190,41 @@ def test_package_request_status_display(user: User, pkgbase: PackageBase): pkgreq.Status = 124 with pytest.raises(KeyError): pkgreq.status_display() + + +def test_package_request_ml_message_id_hash(user: User, pkgbase: PackageBase): + with db.begin(): + pkgreq = db.create( + PackageRequest, + ID=1, + ReqTypeID=MERGE_ID, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), + ClosureComment=str(), + Status=PENDING_ID, + ) + + # A hash composed with ID=1 should result in BNNNRWOFDRSQP4LVPT77FF2GUFR45KW5 + assert pkgreq.ml_message_id_hash() == "BNNNRWOFDRSQP4LVPT77FF2GUFR45KW5" + + +def test_package_request_ml_message_url(user: User, pkgbase: PackageBase): + with db.begin(): + pkgreq = db.create( + PackageRequest, + ID=1, + ReqTypeID=MERGE_ID, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=str(), + ClosureComment=str(), + Status=PENDING_ID, + ) + + assert ( + config.get("options", "ml_thread_url") % (pkgreq.ml_message_id_hash()) + == pkgreq.ml_message_url() + ) diff --git a/test/test_package_source.py b/test/test_package_source.py index e5797f90..06230580 100644 --- a/test/test_package_source.py +++ b/test/test_package_source.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -18,9 +17,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user diff --git a/test/test_package_vote.py b/test/test_package_vote.py index 24d2fdd2..9a868262 100644 --- a/test/test_package_vote.py +++ b/test/test_package_vote.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db, time @@ -17,9 +16,14 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd=str(), - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd=str(), + AccountTypeID=USER_ID, + ) yield user @@ -34,8 +38,7 @@ def test_package_vote_creation(user: User, pkgbase: PackageBase): ts = time.utcnow() with db.begin(): - package_vote = db.create(PackageVote, User=user, - PackageBase=pkgbase, VoteTS=ts) + package_vote = db.create(PackageVote, User=user, PackageBase=pkgbase, VoteTS=ts) assert bool(package_vote) assert package_vote.User == user assert package_vote.PackageBase == pkgbase diff --git a/test/test_packages_routes.py b/test/test_packages_routes.py index ee837912..4bf7a5ae 100644 --- a/test/test_packages_routes.py +++ b/test/test_packages_routes.py @@ -1,14 +1,12 @@ import re - from http import HTTPStatus -from typing import List from unittest import mock import pytest - from fastapi.testclient import TestClient -from aurweb import asgi, db, time +from aurweb import asgi, cache, config, db, time +from aurweb.filters import datetime_display from aurweb.models import License, PackageLicense from aurweb.models.account_type import USER_ID, AccountType from aurweb.models.dependency_type import DependencyType @@ -23,7 +21,12 @@ from aurweb.models.package_notification import PackageNotification from aurweb.models.package_relation import PackageRelation from aurweb.models.package_request import PackageRequest from aurweb.models.package_vote import PackageVote -from aurweb.models.relation_type import CONFLICTS_ID, PROVIDES_ID, REPLACES_ID, RelationType +from aurweb.models.relation_type import ( + CONFLICTS_ID, + PROVIDES_ID, + REPLACES_ID, + RelationType, +) from aurweb.models.request_type import DELETION_ID, RequestType from aurweb.models.user import User from aurweb.testing.html import get_errors, get_successes, parse_root @@ -35,30 +38,24 @@ def package_endpoint(package: Package) -> str: def create_package(pkgname: str, maintainer: User) -> Package: - pkgbase = db.create(PackageBase, - Name=pkgname, - Maintainer=maintainer) + pkgbase = db.create(PackageBase, Name=pkgname, Maintainer=maintainer) return db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase) -def create_package_dep(package: Package, depname: str, - dep_type_name: str = "depends") -> PackageDependency: - dep_type = db.query(DependencyType, - DependencyType.Name == dep_type_name).first() - return db.create(PackageDependency, - DependencyType=dep_type, - Package=package, - DepName=depname) +def create_package_dep( + package: Package, depname: str, dep_type_name: str = "depends" +) -> PackageDependency: + dep_type = db.query(DependencyType, DependencyType.Name == dep_type_name).first() + return db.create( + PackageDependency, DependencyType=dep_type, Package=package, DepName=depname + ) -def create_package_rel(package: Package, - relname: str) -> PackageRelation: - rel_type = db.query(RelationType, - RelationType.ID == PROVIDES_ID).first() - return db.create(PackageRelation, - RelationType=rel_type, - Package=package, - RelName=relname) +def create_package_rel(package: Package, relname: str) -> PackageRelation: + rel_type = db.query(RelationType, RelationType.ID == PROVIDES_ID).first() + return db.create( + PackageRelation, RelationType=rel_type, Package=package, RelName=relname + ) @pytest.fixture(autouse=True) @@ -66,66 +63,102 @@ def setup(db_test): return +@pytest.fixture(autouse=True) +def clear_fakeredis_cache(): + cache._redis.flushall() + + @pytest.fixture def client() -> TestClient: - """ Yield a FastAPI TestClient. """ - yield TestClient(app=asgi.app) + """Yield a FastAPI TestClient.""" + client = TestClient(app=asgi.app) + + # disable redirects for our tests + client.follow_redirects = False + yield client def create_user(username: str) -> User: with db.begin(): - user = db.create(User, Username=username, - Email=f"{username}@example.org", - Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username=username, + Email=f"{username}@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) return user @pytest.fixture def user() -> User: - """ Yield a user. """ + """Yield a user.""" user = create_user("test") yield user @pytest.fixture def maintainer() -> User: - """ Yield a specific User used to maintain packages. """ + """Yield a specific User used to maintain packages.""" account_type = db.query(AccountType, AccountType.ID == USER_ID).first() with db.begin(): - maintainer = db.create(User, Username="test_maintainer", - Email="test_maintainer@example.org", - Passwd="testPassword", - AccountType=account_type) + maintainer = db.create( + User, + Username="test_maintainer", + Email="test_maintainer@example.org", + Passwd="testPassword", + AccountType=account_type, + ) yield maintainer @pytest.fixture -def tu_user(): - tu_type = db.query(AccountType, - AccountType.AccountType == "Trusted User").first() +def pm_user(): + pm_type = db.query( + AccountType, AccountType.AccountType == "Package Maintainer" + ).first() with db.begin(): - tu_user = db.create(User, Username="test_tu", - Email="test_tu@example.org", - RealName="Test TU", Passwd="testPassword", - AccountType=tu_type) - yield tu_user + pm_user = db.create( + User, + Username="test_pm", + Email="test_pm@example.org", + RealName="Test PM", + Passwd="testPassword", + AccountType=pm_type, + ) + yield pm_user + + +@pytest.fixture +def user_who_hates_grey_comments() -> User: + """Yield a specific User who doesn't like grey comments.""" + account_type = db.query(AccountType, AccountType.ID == USER_ID).first() + with db.begin(): + user_who_hates_grey_comments = db.create( + User, + Username="test_hater", + Email="test_hater@example.org", + Passwd="testPassword", + AccountType=account_type, + HideDeletedComments=True, + ) + yield user_who_hates_grey_comments @pytest.fixture def package(maintainer: User) -> Package: - """ Yield a Package created by user. """ + """Yield a Package created by user.""" now = time.utcnow() with db.begin(): - pkgbase = db.create(PackageBase, - Name="test-package", - Maintainer=maintainer, - Packager=maintainer, - Submitter=maintainer, - ModifiedTS=now) - package = db.create(Package, - PackageBase=pkgbase, - Name=pkgbase.Name) + pkgbase = db.create( + PackageBase, + Name="test-package", + Maintainer=maintainer, + Packager=maintainer, + Submitter=maintainer, + ModifiedTS=now, + ) + package = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) yield package @@ -136,29 +169,34 @@ def pkgbase(package: Package) -> PackageBase: @pytest.fixture def target(maintainer: User) -> PackageBase: - """ Merge target. """ + """Merge target.""" now = time.utcnow() with db.begin(): - pkgbase = db.create(PackageBase, Name="target-package", - Maintainer=maintainer, - Packager=maintainer, - Submitter=maintainer, - ModifiedTS=now) + pkgbase = db.create( + PackageBase, + Name="target-package", + Maintainer=maintainer, + Packager=maintainer, + Submitter=maintainer, + ModifiedTS=now, + ) db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) yield pkgbase @pytest.fixture def pkgreq(user: User, pkgbase: PackageBase) -> PackageRequest: - """ Yield a PackageRequest related to `pkgbase`. """ + """Yield a PackageRequest related to `pkgbase`.""" with db.begin(): - pkgreq = db.create(PackageRequest, - ReqTypeID=DELETION_ID, - User=user, - PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Comments=f"Deletion request for {pkgbase.Name}", - ClosureComment=str()) + pkgreq = db.create( + PackageRequest, + ReqTypeID=DELETION_ID, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=f"Deletion request for {pkgbase.Name}", + ClosureComment=str(), + ) yield pkgreq @@ -167,31 +205,50 @@ def comment(user: User, package: Package) -> PackageComment: pkgbase = package.PackageBase now = time.utcnow() with db.begin(): - comment = db.create(PackageComment, - User=user, - PackageBase=pkgbase, - Comments="Test comment.", - RenderedComment=str(), - CommentTS=now) + comment = db.create( + PackageComment, + User=user, + PackageBase=pkgbase, + Comments="Test comment.", + RenderedComment=str(), + CommentTS=now, + ) yield comment @pytest.fixture -def packages(maintainer: User) -> List[Package]: - """ Yield 55 packages named pkg_0 .. pkg_54. """ +def deleted_comment(user: User, package: Package) -> PackageComment: + pkgbase = package.PackageBase + now = time.utcnow() + with db.begin(): + comment = db.create( + PackageComment, + User=user, + PackageBase=pkgbase, + Comments="Test comment.", + RenderedComment=str(), + CommentTS=now, + DelTS=now, + ) + yield comment + + +@pytest.fixture +def packages(maintainer: User) -> list[Package]: + """Yield 55 packages named pkg_0 .. pkg_54.""" packages_ = [] now = time.utcnow() with db.begin(): for i in range(55): - pkgbase = db.create(PackageBase, - Name=f"pkg_{i}", - Maintainer=maintainer, - Packager=maintainer, - Submitter=maintainer, - ModifiedTS=now) - package = db.create(Package, - PackageBase=pkgbase, - Name=f"pkg_{i}") + pkgbase = db.create( + PackageBase, + Name=f"pkg_{i}", + Maintainer=maintainer, + Packager=maintainer, + Submitter=maintainer, + ModifiedTS=now, + ) + package = db.create(Package, PackageBase=pkgbase, Name=f"pkg_{i}") packages_.append(package) yield packages_ @@ -204,40 +261,63 @@ def test_package_not_found(client: TestClient): def test_package(client: TestClient, package: Package): - """ Test a single / packages / {name} route. """ + """Test a single / packages / {name} route.""" with db.begin(): - db.create(PackageRelation, PackageID=package.ID, - RelTypeID=PROVIDES_ID, - RelName="test_provider1") - db.create(PackageRelation, PackageID=package.ID, - RelTypeID=PROVIDES_ID, - RelName="test_provider2") + db.create( + PackageRelation, + PackageID=package.ID, + RelTypeID=PROVIDES_ID, + RelName="test_provider1", + ) + db.create( + PackageRelation, + PackageID=package.ID, + RelTypeID=PROVIDES_ID, + RelName="test_provider2", + ) - db.create(PackageRelation, PackageID=package.ID, - RelTypeID=REPLACES_ID, - RelName="test_replacer1") - db.create(PackageRelation, PackageID=package.ID, - RelTypeID=REPLACES_ID, - RelName="test_replacer2") + db.create( + PackageRelation, + PackageID=package.ID, + RelTypeID=REPLACES_ID, + RelName="test_replacer1", + ) + db.create( + PackageRelation, + PackageID=package.ID, + RelTypeID=REPLACES_ID, + RelName="test_replacer2", + ) - db.create(PackageRelation, PackageID=package.ID, - RelTypeID=CONFLICTS_ID, - RelName="test_conflict1") - db.create(PackageRelation, PackageID=package.ID, - RelTypeID=CONFLICTS_ID, - RelName="test_conflict2") + db.create( + PackageRelation, + PackageID=package.ID, + RelTypeID=CONFLICTS_ID, + RelName="test_conflict1", + ) + db.create( + PackageRelation, + PackageID=package.ID, + RelTypeID=CONFLICTS_ID, + RelName="test_conflict2", + ) # Create some licenses. licenses = [ db.create(License, Name="test_license1"), - db.create(License, Name="test_license2") + db.create(License, Name="test_license2"), ] - db.create(PackageLicense, PackageID=package.ID, - License=licenses[0]) - db.create(PackageLicense, PackageID=package.ID, - License=licenses[1]) + db.create(PackageLicense, PackageID=package.ID, License=licenses[0]) + db.create(PackageLicense, PackageID=package.ID, License=licenses[1]) + + # Create some keywords + keywords = ["test1", "test2"] + for keyword in keywords: + db.create( + PackageKeyword, PackageBaseID=package.PackageBaseID, Keyword=keyword + ) with client as request: resp = request.get(package_endpoint(package)) @@ -275,32 +355,161 @@ def test_package(client: TestClient, package: Package): expected = ["test_conflict1", "test_conflict2"] assert conflicts[0].text.strip() == ", ".join(expected) + keywords = root.xpath('//a[@class="keyword"]') + expected = ["test1", "test2"] + for i, keyword in enumerate(expected): + assert keywords[i].text.strip() == keyword -def test_package_comments(client: TestClient, user: User, package: Package): - now = (time.utcnow()) + +def test_package_split_description(client: TestClient, user: User): with db.begin(): - comment = db.create(PackageComment, PackageBase=package.PackageBase, - User=user, Comments="Test comment", CommentTS=now) + pkgbase = db.create( + PackageBase, + Name="pkgbase", + Maintainer=user, + Packager=user, + ) + + pkg_a = db.create( + Package, + PackageBase=pkgbase, + Name="pkg_a", + Description="pkg_a desc", + ) + pkg_b = db.create( + Package, + PackageBase=pkgbase, + Name="pkg_b", + Description="pkg_b desc", + ) + + # Check pkg_a + with client as request: + endp = f"/packages/{pkg_a.Name}" + resp = request.get(endp) + assert resp.status_code == HTTPStatus.OK + + root = parse_root(resp.text) + row = root.xpath('//tr[@id="pkg-description"]/td')[0] + assert row.text == pkg_a.Description + + # Check pkg_b + with client as request: + endp = f"/packages/{pkg_b.Name}" + resp = request.get(endp) + assert resp.status_code == HTTPStatus.OK + + root = parse_root(resp.text) + row = root.xpath('//tr[@id="pkg-description"]/td')[0] + assert row.text == pkg_b.Description + + +def test_paged_depends_required(client: TestClient, package: Package): + maint = package.PackageBase.Maintainer + new_pkgs = [] + + with db.begin(): + # Create 25 new packages that'll be used to depend on our package. + for i in range(26): + base = db.create(PackageBase, Name=f"new_pkg{i}", Maintainer=maint) + new_pkgs.append(db.create(Package, Name=base.Name, PackageBase=base)) + + # Create 25 deps. + for i in range(25): + create_package_dep(package, f"dep_{i}") + + with db.begin(): + # Create depends on this package so we get some required by listings. + for new_pkg in new_pkgs: + create_package_dep(new_pkg, package.Name) + + with client as request: + resp = request.get(package_endpoint(package)) + assert resp.status_code == int(HTTPStatus.OK) + + # Test depends show link. + assert "Show 5 more" in resp.text + + # Test required by show more link, we added 26 packages. + assert "Show 6 more" in resp.text + + # Follow both links at the same time. + with client as request: + resp = request.get( + package_endpoint(package), + params={ + "all_deps": True, + "all_reqs": True, + }, + ) + assert resp.status_code == int(HTTPStatus.OK) + + # We're should see everything and have no link. + assert "Show 5 more" not in resp.text + assert "Show 6 more" not in resp.text + + +def test_package_comments( + client: TestClient, user: User, user_who_hates_grey_comments: User, package: Package +): + now = time.utcnow() + with db.begin(): + comment = db.create( + PackageComment, + PackageBase=package.PackageBase, + User=user, + Comments="Test comment", + CommentTS=now, + ) + deleted_comment = db.create( + PackageComment, + PackageBase=package.PackageBase, + User=user, + Comments="Deleted Test comment", + CommentTS=now, + DelTS=now - 1, + ) cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get(package_endpoint(package), cookies=cookies) + request.cookies = cookies + resp = request.get(package_endpoint(package)) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) - expected = [ - comment.Comments - ] - comments = root.xpath('.//div[contains(@class, "package-comments")]' - '/div[@class="article-content"]/div/text()') + expected = [comment.Comments, deleted_comment.Comments] + comments = root.xpath( + './/div[contains(@class, "package-comments")]' + '/div[@class="article-content"]/div/text()' + ) + assert len(comments) == 2 + for i, row in enumerate(expected): + assert comments[i].strip() == row + + cookies = {"AURSID": user_who_hates_grey_comments.login(Request(), "testPassword")} + with client as request: + request.cookies = cookies + resp = request.get(package_endpoint(package)) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + expected = [comment.Comments] + comments = root.xpath( + './/div[contains(@class, "package-comments")]' + '/div[@class="article-content"]/div/text()' + ) + assert len(comments) == 1 # Deleted comment is hidden for i, row in enumerate(expected): assert comments[i].strip() == row -def test_package_requests_display(client: TestClient, user: User, - package: Package, pkgreq: PackageRequest): +def test_package_requests_display( + client: TestClient, user: User, package: Package, pkgreq: PackageRequest +): # Test that a single request displays "1 pending request". + cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: + request.cookies = cookies resp = request.get(package_endpoint(package)) assert resp.status_code == int(HTTPStatus.OK) @@ -311,14 +520,19 @@ def test_package_requests_display(client: TestClient, user: User, type_ = db.query(RequestType, RequestType.ID == DELETION_ID).first() with db.begin(): - db.create(PackageRequest, PackageBase=package.PackageBase, - PackageBaseName=package.PackageBase.Name, - User=user, RequestType=type_, - Comments="Test comment2.", - ClosureComment=str()) + db.create( + PackageRequest, + PackageBase=package.PackageBase, + PackageBaseName=package.PackageBase.Name, + User=user, + RequestType=type_, + Comments="Test comment2.", + ClosureComment=str(), + ) # Test that a two requests display "2 pending requests". with client as request: + request.cookies = cookies resp = request.get(package_endpoint(package)) assert resp.status_code == int(HTTPStatus.OK) @@ -328,14 +542,14 @@ def test_package_requests_display(client: TestClient, user: User, assert target.text.strip() == "2 pending requests" -def test_package_authenticated(client: TestClient, user: User, - package: Package): - """ We get the same here for either authenticated or not +def test_package_authenticated(client: TestClient, user: User, package: Package): + """We get the same here for either authenticated or not authenticated. Form inputs are presented to maintainers. - This process also occurs when pkgbase.html is rendered. """ + This process also occurs when pkgbase.html is rendered.""" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get(package_endpoint(package), cookies=cookies) + request.cookies = cookies + resp = request.get(package_endpoint(package)) assert resp.status_code == int(HTTPStatus.OK) expected = [ @@ -346,11 +560,21 @@ def test_package_authenticated(client: TestClient, user: User, "Flag package out-of-date", "Vote for this package", "Enable notifications", - "Submit Request" + "Submit Request", ] for expected_text in expected: assert expected_text in resp.text + # make sure we don't have these. Only for Maintainer/TUs/Devs + not_expected = [ + "Disown Package", + "View Requests", + "Delete Package", + "Merge Package", + ] + for unexpected_text in not_expected: + assert unexpected_text not in resp.text + # When no requests are up, make sure we don't see the display for them. root = parse_root(resp.text) selector = '//div[@id="actionlist"]/ul/li/span[@class="flagged"]' @@ -358,12 +582,13 @@ def test_package_authenticated(client: TestClient, user: User, assert len(target) == 0 -def test_package_authenticated_maintainer(client: TestClient, - maintainer: User, - package: Package): +def test_package_authenticated_maintainer( + client: TestClient, maintainer: User, package: Package +): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - resp = request.get(package_endpoint(package), cookies=cookies) + request.cookies = cookies + resp = request.get(package_endpoint(package)) assert resp.status_code == int(HTTPStatus.OK) expected = [ @@ -376,18 +601,28 @@ def test_package_authenticated_maintainer(client: TestClient, "Enable notifications", "Manage Co-Maintainers", "Submit Request", - "Disown Package" + "Disown Package", ] for expected_text in expected: assert expected_text in resp.text + # make sure we don't have these. Only for PMs/Devs + not_expected = [ + "1 pending request", + "Delete Package", + "Merge Package", + ] + for unexpected_text in not_expected: + assert unexpected_text not in resp.text -def test_package_authenticated_tu(client: TestClient, - tu_user: User, - package: Package): - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + +def test_package_authenticated_pm( + client: TestClient, pm_user: User, package: Package, pkgreq: PackageRequest +): + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: - resp = request.get(package_endpoint(package), cookies=cookies) + request.cookies = cookies + resp = request.get(package_endpoint(package)) assert resp.status_code == int(HTTPStatus.OK) expected = [ @@ -399,17 +634,17 @@ def test_package_authenticated_tu(client: TestClient, "Vote for this package", "Enable notifications", "Manage Co-Maintainers", + "1 pending request", "Submit Request", "Delete Package", "Merge Package", - "Disown Package" + "Disown Package", ] for expected_text in expected: assert expected_text in resp.text -def test_package_dependencies(client: TestClient, maintainer: User, - package: Package): +def test_package_dependencies(client: TestClient, maintainer: User, package: Package): # Create a normal dependency of type depends. with db.begin(): dep_pkg = create_package("test-dep-1", maintainer) @@ -417,32 +652,32 @@ def test_package_dependencies(client: TestClient, maintainer: User, # Also, create a makedepends. make_dep_pkg = create_package("test-dep-2", maintainer) - make_dep = create_package_dep(package, make_dep_pkg.Name, - dep_type_name="makedepends") + make_dep = create_package_dep( + package, make_dep_pkg.Name, dep_type_name="makedepends" + ) make_dep.DepArch = "x86_64" # And... a checkdepends! check_dep_pkg = create_package("test-dep-3", maintainer) - create_package_dep(package, check_dep_pkg.Name, - dep_type_name="checkdepends") + create_package_dep(package, check_dep_pkg.Name, dep_type_name="checkdepends") # Geez. Just stop. This is optdepends. opt_dep_pkg = create_package("test-dep-4", maintainer) - create_package_dep(package, opt_dep_pkg.Name, - dep_type_name="optdepends") + create_package_dep(package, opt_dep_pkg.Name, dep_type_name="optdepends") # Heh. Another optdepends to test one with a description. opt_desc_dep_pkg = create_package("test-dep-5", maintainer) - opt_desc_dep = create_package_dep(package, opt_desc_dep_pkg.Name, - dep_type_name="optdepends") + opt_desc_dep = create_package_dep( + package, opt_desc_dep_pkg.Name, dep_type_name="optdepends" + ) opt_desc_dep.DepDesc = "Test description." - broken_dep = create_package_dep(package, "test-dep-6", - dep_type_name="depends") + broken_dep = create_package_dep(package, "test-dep-6", dep_type_name="depends") # Create an official provider record. - db.create(OfficialProvider, Name="test-dep-99", - Repo="core", Provides="test-dep-99") + db.create( + OfficialProvider, Name="test-dep-99", Repo="core", Provides="test-dep-99" + ) create_package_dep(package, "test-dep-99") # Also, create a provider who provides our test-dep-99. @@ -454,13 +689,14 @@ def test_package_dependencies(client: TestClient, maintainer: User, assert resp.status_code == int(HTTPStatus.OK) # Let's make sure all the non-broken deps are ordered as we expect. - expected = list(filter( - lambda e: e.is_package(), - package.package_dependencies.order_by( - PackageDependency.DepTypeID.asc(), - PackageDependency.DepName.asc() - ).all() - )) + expected = list( + filter( + lambda e: e.is_package(), + package.package_dependencies.order_by( + PackageDependency.DepTypeID.asc(), PackageDependency.DepName.asc() + ).all(), + ) + ) root = parse_root(resp.text) pkgdeps = root.findall('.//ul[@id="pkgdepslist"]/li/a') for i, expectation in enumerate(expected): @@ -468,7 +704,7 @@ def test_package_dependencies(client: TestClient, maintainer: User, # Let's make sure the DepArch was displayed for our target make dep. arch = root.findall('.//ul[@id="pkgdepslist"]/li')[3] - arch = arch.xpath('./em')[0] + arch = arch.xpath("./em")[0] assert arch.text.strip() == "(make, x86_64)" # And let's make sure that the broken package was displayed. @@ -476,18 +712,21 @@ def test_package_dependencies(client: TestClient, maintainer: User, assert broken_node.text.strip() == broken_dep.DepName -def test_packages(client: TestClient, packages: List[Package]): +def test_packages(client: TestClient, packages: list[Package]): with client as request: - response = request.get("/packages", params={ - "SeB": "X", # "X" isn't valid, defaults to "nd" - "PP": "1 or 1", - "O": "0 or 0" - }) + response = request.get( + "/packages", + params={ + "SeB": "X", # "X" isn't valid, defaults to "nd" + "PP": "1 or 1", + "O": "0 or 0", + }, + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) stats = root.xpath('//div[@class="pkglist-stats"]/p')[0] - pager_text = re.sub(r'\s+', " ", stats.text.replace("\n", "").strip()) + pager_text = re.sub(r"\s+", " ", stats.text.replace("\n", "").strip()) assert pager_text == "55 packages found. Page 1 of 2." rows = root.xpath('//table[@class="results"]/tbody/tr') @@ -505,12 +744,9 @@ def test_packages_empty(client: TestClient): assert results[0].text.strip() == expected -def test_packages_search_by_name(client: TestClient, packages: List[Package]): +def test_packages_search_by_name(client: TestClient, packages: list[Package]): with client as request: - response = request.get("/packages", params={ - "SeB": "n", - "K": "pkg_" - }) + response = request.get("/packages", params={"SeB": "n", "K": "pkg_"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -519,13 +755,9 @@ def test_packages_search_by_name(client: TestClient, packages: List[Package]): assert len(rows) == 50 # Default per-page -def test_packages_search_by_exact_name(client: TestClient, - packages: List[Package]): +def test_packages_search_by_exact_name(client: TestClient, packages: list[Package]): with client as request: - response = request.get("/packages", params={ - "SeB": "N", - "K": "pkg_" - }) + response = request.get("/packages", params={"SeB": "N", "K": "pkg_"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -535,10 +767,7 @@ def test_packages_search_by_exact_name(client: TestClient, assert len(rows) == 0 with client as request: - response = request.get("/packages", params={ - "SeB": "N", - "K": "pkg_1" - }) + response = request.get("/packages", params={"SeB": "N", "K": "pkg_1"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -548,13 +777,9 @@ def test_packages_search_by_exact_name(client: TestClient, assert len(rows) == 1 -def test_packages_search_by_pkgbase(client: TestClient, - packages: List[Package]): +def test_packages_search_by_pkgbase(client: TestClient, packages: list[Package]): with client as request: - response = request.get("/packages", params={ - "SeB": "b", - "K": "pkg_" - }) + response = request.get("/packages", params={"SeB": "b", "K": "pkg_"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -563,13 +788,9 @@ def test_packages_search_by_pkgbase(client: TestClient, assert len(rows) == 50 -def test_packages_search_by_exact_pkgbase(client: TestClient, - packages: List[Package]): +def test_packages_search_by_exact_pkgbase(client: TestClient, packages: list[Package]): with client as request: - response = request.get("/packages", params={ - "SeB": "B", - "K": "pkg_" - }) + response = request.get("/packages", params={"SeB": "B", "K": "pkg_"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -577,10 +798,7 @@ def test_packages_search_by_exact_pkgbase(client: TestClient, assert len(rows) == 0 with client as request: - response = request.get("/packages", params={ - "SeB": "B", - "K": "pkg_1" - }) + response = request.get("/packages", params={"SeB": "B", "K": "pkg_1"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -588,14 +806,10 @@ def test_packages_search_by_exact_pkgbase(client: TestClient, assert len(rows) == 1 -def test_packages_search_by_keywords(client: TestClient, - packages: List[Package]): +def test_packages_search_by_keywords(client: TestClient, packages: list[Package]): # None of our packages have keywords, so this query should return nothing. with client as request: - response = request.get("/packages", params={ - "SeB": "k", - "K": "testKeyword" - }) + response = request.get("/packages", params={"SeB": "k", "K": "testKeyword"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -605,16 +819,32 @@ def test_packages_search_by_keywords(client: TestClient, # But now, let's create the keyword for the first package. package = packages[0] with db.begin(): - db.create(PackageKeyword, - PackageBase=package.PackageBase, - Keyword="testKeyword") + db.create( + PackageKeyword, PackageBase=package.PackageBase, Keyword="testKeyword" + ) # And request packages with that keyword, we should get 1 result. with client as request: - response = request.get("/packages", params={ - "SeB": "k", - "K": "testKeyword" - }) + # clear fakeredis cache + cache._redis.flushall() + response = request.get("/packages", params={"SeB": "k", "K": "testKeyword"}) + assert response.status_code == int(HTTPStatus.OK) + + root = parse_root(response.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + + # Now let's add another keyword to the same package + with db.begin(): + db.create( + PackageKeyword, PackageBase=package.PackageBase, Keyword="testKeyword2" + ) + + # And request packages with both keywords, we should still get 1 result. + with client as request: + response = request.get( + "/packages", params={"SeB": "k", "K": "testKeyword testKeyword2"} + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -622,16 +852,15 @@ def test_packages_search_by_keywords(client: TestClient, assert len(rows) == 1 -def test_packages_search_by_maintainer(client: TestClient, - maintainer: User, - package: Package): +def test_packages_search_by_maintainer( + client: TestClient, maintainer: User, package: Package +): # We should expect that searching by `package`'s maintainer # returns `package` in the results. with client as request: - response = request.get("/packages", params={ - "SeB": "m", - "K": maintainer.Username - }) + response = request.get( + "/packages", params={"SeB": "m", "K": maintainer.Username} + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr') @@ -653,6 +882,8 @@ def test_packages_search_by_maintainer(client: TestClient, # This time, we should get `package` returned, since it's now an orphan. with client as request: + # clear fakeredis cache + cache._redis.flushall() response = request.get("/packages", params={"SeB": "m"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -660,15 +891,14 @@ def test_packages_search_by_maintainer(client: TestClient, assert len(rows) == 1 -def test_packages_search_by_comaintainer(client: TestClient, - maintainer: User, - package: Package): +def test_packages_search_by_comaintainer( + client: TestClient, maintainer: User, package: Package +): # Nobody's a comaintainer yet. with client as request: - response = request.get("/packages", params={ - "SeB": "c", - "K": maintainer.Username - }) + response = request.get( + "/packages", params={"SeB": "c", "K": maintainer.Username} + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -677,17 +907,20 @@ def test_packages_search_by_comaintainer(client: TestClient, # Now, we create a comaintainer. with db.begin(): - db.create(PackageComaintainer, - PackageBase=package.PackageBase, - User=maintainer, - Priority=1) + db.create( + PackageComaintainer, + PackageBase=package.PackageBase, + User=maintainer, + Priority=1, + ) # Then test that it's returned by our search. with client as request: - response = request.get("/packages", params={ - "SeB": "c", - "K": maintainer.Username - }) + # clear fakeredis cache + cache._redis.flushall() + response = request.get( + "/packages", params={"SeB": "c", "K": maintainer.Username} + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -695,15 +928,18 @@ def test_packages_search_by_comaintainer(client: TestClient, assert len(rows) == 1 -def test_packages_search_by_co_or_maintainer(client: TestClient, - maintainer: User, - package: Package): +def test_packages_search_by_co_or_maintainer( + client: TestClient, maintainer: User, package: Package +): with client as request: - response = request.get("/packages", params={ - "SeB": "M", - "SB": "BLAH", # Invalid SB; gets reset to default "n". - "K": maintainer.Username - }) + response = request.get( + "/packages", + params={ + "SeB": "M", + "SB": "BLAH", # Invalid SB; gets reset to default "n". + "K": maintainer.Username, + }, + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -711,19 +947,18 @@ def test_packages_search_by_co_or_maintainer(client: TestClient, assert len(rows) == 1 with db.begin(): - user = db.create(User, Username="comaintainer", - Email="comaintainer@example.org", - Passwd="testPassword") - db.create(PackageComaintainer, - PackageBase=package.PackageBase, - User=user, - Priority=1) + user = db.create( + User, + Username="comaintainer", + Email="comaintainer@example.org", + Passwd="testPassword", + ) + db.create( + PackageComaintainer, PackageBase=package.PackageBase, User=user, Priority=1 + ) with client as request: - response = request.get("/packages", params={ - "SeB": "M", - "K": user.Username - }) + response = request.get("/packages", params={"SeB": "M", "K": user.Username}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -731,14 +966,13 @@ def test_packages_search_by_co_or_maintainer(client: TestClient, assert len(rows) == 1 -def test_packages_search_by_submitter(client: TestClient, - maintainer: User, - package: Package): +def test_packages_search_by_submitter( + client: TestClient, maintainer: User, package: Package +): with client as request: - response = request.get("/packages", params={ - "SeB": "s", - "K": maintainer.Username - }) + response = request.get( + "/packages", params={"SeB": "s", "K": maintainer.Username} + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) @@ -746,186 +980,186 @@ def test_packages_search_by_submitter(client: TestClient, assert len(rows) == 1 -def test_packages_sort_by_name(client: TestClient, packages: List[Package]): +def test_packages_sort_by_name(client: TestClient, packages: list[Package]): with client as request: - response = request.get("/packages", params={ - "SB": "n", # Name - "SO": "a", # Ascending - "PP": "150" - }) + response = request.get( + "/packages", params={"SB": "n", "SO": "a", "PP": "150"} # Name # Ascending + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr') - rows = [row.xpath('./td/a')[0].text.strip() for row in rows] + rows = [row.xpath("./td/a")[0].text.strip() for row in rows] with client as request: - response2 = request.get("/packages", params={ - "SB": "n", # Name - "SO": "d", # Ascending - "PP": "150" - }) + response2 = request.get( + "/packages", params={"SB": "n", "SO": "d", "PP": "150"} # Name # Ascending + ) assert response2.status_code == int(HTTPStatus.OK) root = parse_root(response2.text) rows2 = root.xpath('//table[@class="results"]/tbody/tr') - rows2 = [row.xpath('./td/a')[0].text.strip() for row in rows2] + rows2 = [row.xpath("./td/a")[0].text.strip() for row in rows2] assert rows == list(reversed(rows2)) -def test_packages_sort_by_votes(client: TestClient, - maintainer: User, - packages: List[Package]): +def test_packages_sort_by_votes( + client: TestClient, maintainer: User, packages: list[Package] +): # Set the first package's NumVotes to 1. with db.begin(): packages[0].PackageBase.NumVotes = 1 # Test that, by default, the first result is what we just set above. with client as request: - response = request.get("/packages", params={ - "SB": "v" # Votes. - }) + response = request.get("/packages", params={"SB": "v"}) # Votes. assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr') - votes = rows[0].xpath('./td')[2] # The third column of the first row. + votes = rows[0].xpath("./td")[2] # The third column of the first row. assert votes.text.strip() == "1" # Now, test that with an ascending order, the last result is # the one we set, since the default (above) is descending. with client as request: - response = request.get("/packages", params={ - "SB": "v", # Votes. - "SO": "a", # Ascending. - "O": "50" # Second page. - }) + response = request.get( + "/packages", + params={ + "SB": "v", # Votes. + "SO": "a", # Ascending. + "O": "50", # Second page. + }, + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr') - votes = rows[-1].xpath('./td')[2] # The third column of the last row. + votes = rows[-1].xpath("./td")[2] # The third column of the last row. assert votes.text.strip() == "1" -def test_packages_sort_by_popularity(client: TestClient, - maintainer: User, - packages: List[Package]): +def test_packages_sort_by_popularity( + client: TestClient, maintainer: User, packages: list[Package] +): # Set the first package's Popularity to 0.50. with db.begin(): packages[0].PackageBase.Popularity = "0.50" # Test that, by default, the first result is what we just set above. with client as request: - response = request.get("/packages", params={ - "SB": "p" # Popularity - }) + response = request.get("/packages", params={"SB": "p"}) # Popularity assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr') - pop = rows[0].xpath('./td')[3] # The fourth column of the first row. + pop = rows[0].xpath("./td")[3] # The fourth column of the first row. assert pop.text.strip() == "0.50" -def test_packages_sort_by_voted(client: TestClient, - maintainer: User, - packages: List[Package]): +def test_packages_sort_by_voted( + client: TestClient, maintainer: User, packages: list[Package] +): now = time.utcnow() with db.begin(): - db.create(PackageVote, PackageBase=packages[0].PackageBase, - User=maintainer, VoteTS=now) + db.create( + PackageVote, + PackageBase=packages[0].PackageBase, + User=maintainer, + VoteTS=now, + ) # Test that, by default, the first result is what we just set above. cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - response = request.get("/packages", params={ - "SB": "w", # Voted - "SO": "d" # Descending, Voted first. - }, cookies=cookies) + request.cookies = cookies + response = request.get( + "/packages", + params={"SB": "w", "SO": "d"}, # Voted # Descending, Voted first. + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr') - voted = rows[0].xpath('./td')[5] # The sixth column of the first row. + voted = rows[0].xpath("./td")[5] # The sixth column of the first row. assert voted.text.strip() == "Yes" # Conversely, everything else was not voted on. - voted = rows[1].xpath('./td')[5] # The sixth column of the second row. + voted = rows[1].xpath("./td")[5] # The sixth column of the second row. assert voted.text.strip() == str() # Empty. -def test_packages_sort_by_notify(client: TestClient, - maintainer: User, - packages: List[Package]): - db.create(PackageNotification, - PackageBase=packages[0].PackageBase, - User=maintainer) +def test_packages_sort_by_notify( + client: TestClient, maintainer: User, packages: list[Package] +): + db.create(PackageNotification, PackageBase=packages[0].PackageBase, User=maintainer) # Test that, by default, the first result is what we just set above. cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - response = request.get("/packages", params={ - "SB": "o", # Voted - "SO": "d" # Descending, Voted first. - }, cookies=cookies) + request.cookies = cookies + response = request.get( + "/packages", + params={"SB": "o", "SO": "d"}, # Voted # Descending, Voted first. + ) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr') - notify = rows[0].xpath('./td')[6] # The sixth column of the first row. + notify = rows[0].xpath("./td")[6] # The sixth column of the first row. assert notify.text.strip() == "Yes" # Conversely, everything else was not voted on. - notify = rows[1].xpath('./td')[6] # The sixth column of the second row. + notify = rows[1].xpath("./td")[6] # The sixth column of the second row. assert notify.text.strip() == str() # Empty. -def test_packages_sort_by_maintainer(client: TestClient, - maintainer: User, - package: Package): - """ Sort a package search by the maintainer column. """ +def test_packages_sort_by_maintainer( + client: TestClient, maintainer: User, package: Package +): + """Sort a package search by the maintainer column.""" # Create a second package, so the two can be ordered and checked. with db.begin(): - maintainer2 = db.create(User, Username="maintainer2", - Email="maintainer2@example.org", - Passwd="testPassword") - base2 = db.create(PackageBase, Name="pkg_2", Maintainer=maintainer2, - Submitter=maintainer2, Packager=maintainer2) + maintainer2 = db.create( + User, + Username="maintainer2", + Email="maintainer2@example.org", + Passwd="testPassword", + ) + base2 = db.create( + PackageBase, + Name="pkg_2", + Maintainer=maintainer2, + Submitter=maintainer2, + Packager=maintainer2, + ) db.create(Package, Name="pkg_2", PackageBase=base2) # Check the descending order route. with client as request: - response = request.get("/packages", params={ - "SB": "m", - "SO": "d" - }) + response = request.get("/packages", params={"SB": "m", "SO": "d"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr') - col = rows[0].xpath('./td')[5].xpath('./a')[0] # Last column. + col = rows[0].xpath("./td")[5].xpath("./a")[0] # Last column. assert col.text.strip() == maintainer.Username # On the other hand, with ascending, we should get reverse ordering. with client as request: - response = request.get("/packages", params={ - "SB": "m", - "SO": "a" - }) + response = request.get("/packages", params={"SB": "m", "SO": "a"}) assert response.status_code == int(HTTPStatus.OK) root = parse_root(response.text) rows = root.xpath('//table[@class="results"]/tbody/tr') - col = rows[0].xpath('./td')[5].xpath('./a')[0] # Last column. + col = rows[0].xpath("./td")[5].xpath("./a")[0] # Last column. assert col.text.strip() == maintainer2.Username -def test_packages_sort_by_last_modified(client: TestClient, - packages: List[Package]): +def test_packages_sort_by_last_modified(client: TestClient, packages: list[Package]): now = time.utcnow() # Set the first package's ModifiedTS to be 1000 seconds before now. package = packages[0] @@ -933,10 +1167,10 @@ def test_packages_sort_by_last_modified(client: TestClient, package.PackageBase.ModifiedTS = now - 1000 with client as request: - response = request.get("/packages", params={ - "SB": "l", - "SO": "a" # Ascending; oldest modification first. - }) + response = request.get( + "/packages", + params={"SB": "l", "SO": "a"}, # Ascending; oldest modification first. + ) assert response.status_code == int(HTTPStatus.OK) # We should have 50 (default per page) results. @@ -946,12 +1180,18 @@ def test_packages_sort_by_last_modified(client: TestClient, # Let's assert that the first item returned was the one we modified above. row = rows[0] - col = row.xpath('./td')[0].xpath('./a')[0] + col = row.xpath("./td")[0].xpath("./a")[0] assert col.text.strip() == package.Name + # Make sure our row contains the modified date we've set + tz = config.get("options", "default_timezone") + dt = datetime_display({"timezone": tz}, package.PackageBase.ModifiedTS) + assert dt in "".join(row.itertext()) -def test_packages_flagged(client: TestClient, maintainer: User, - packages: List[Package]): + +def test_packages_flagged( + client: TestClient, maintainer: User, packages: list[Package] +): package = packages[0] now = time.utcnow() @@ -961,9 +1201,7 @@ def test_packages_flagged(client: TestClient, maintainer: User, package.PackageBase.Flagger = maintainer with client as request: - response = request.get("/packages", params={ - "outdated": "on" - }) + response = request.get("/packages", params={"outdated": "on"}) assert response.status_code == int(HTTPStatus.OK) # We should only get one result from this query; the package we flagged. @@ -972,9 +1210,7 @@ def test_packages_flagged(client: TestClient, maintainer: User, assert len(rows) == 1 with client as request: - response = request.get("/packages", params={ - "outdated": "off" - }) + response = request.get("/packages", params={"outdated": "off"}) assert response.status_code == int(HTTPStatus.OK) # In this case, we should get 54 results, which means that the first @@ -984,7 +1220,7 @@ def test_packages_flagged(client: TestClient, maintainer: User, assert len(rows) == 50 -def test_packages_orphans(client: TestClient, packages: List[Package]): +def test_packages_orphans(client: TestClient, packages: list[Package]): package = packages[0] with db.begin(): package.PackageBase.Maintainer = None @@ -1000,14 +1236,17 @@ def test_packages_orphans(client: TestClient, packages: List[Package]): def test_packages_per_page(client: TestClient, maintainer: User): - """ Test the ability for /packages to deal with the PP query - argument specifications (50, 100, 250; default: 50). """ + """Test the ability for /packages to deal with the PP query + argument specifications (50, 100, 250; default: 50).""" with db.begin(): for i in range(255): - base = db.create(PackageBase, Name=f"pkg_{i}", - Maintainer=maintainer, - Submitter=maintainer, - Packager=maintainer) + base = db.create( + PackageBase, + Name=f"pkg_{i}", + Maintainer=maintainer, + Submitter=maintainer, + Packager=maintainer, + ) db.create(Package, PackageBase=base, Name=base.Name) # Test default case, PP of 50. @@ -1035,27 +1274,30 @@ def test_packages_per_page(client: TestClient, maintainer: User): assert len(rows) == 250 -def test_packages_post_unknown_action(client: TestClient, user: User, - package: Package): - +def test_packages_post_unknown_action(client: TestClient, user: User, package: Package): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={"action": "unknown"}, - cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.post( + "/packages", + data={"action": "unknown"}, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) def test_packages_post_error(client: TestClient, user: User, package: Package): - async def stub_action(request: Request, **kwargs): - return (False, ["Some error."]) + return False, ["Some error."] actions = {"stub": stub_action} with mock.patch.dict("aurweb.routers.packages.PACKAGE_ACTIONS", actions): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={"action": "stub"}, - cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.post( + "/packages", + data={"action": "stub"}, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) @@ -1064,16 +1306,18 @@ def test_packages_post_error(client: TestClient, user: User, package: Package): def test_packages_post(client: TestClient, user: User, package: Package): - async def stub_action(request: Request, **kwargs): - return (True, ["Some success."]) + return True, ["Some success."] actions = {"stub": stub_action} with mock.patch.dict("aurweb.routers.packages.PACKAGE_ACTIONS", actions): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={"action": "stub"}, - cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.post( + "/packages", + data={"action": "stub"}, + ) assert resp.status_code == int(HTTPStatus.OK) errors = get_successes(resp.text) @@ -1081,8 +1325,9 @@ def test_packages_post(client: TestClient, user: User, package: Package): assert errors[0].text.strip() == expected -def test_packages_post_unflag(client: TestClient, user: User, - maintainer: User, package: Package): +def test_packages_post_unflag( + client: TestClient, user: User, maintainer: User, package: Package +): # Flag `package` as `user`. now = time.utcnow() with db.begin(): @@ -1094,7 +1339,8 @@ def test_packages_post_unflag(client: TestClient, user: User, # Don't supply any packages. post_data = {"action": "unflag", "IDs": []} with client as request: - resp = request.post("/packages", data=post_data, cookies=cookies) + request.cookies = cookies + resp = request.post("/packages", data=post_data) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages to unflag." @@ -1103,7 +1349,8 @@ def test_packages_post_unflag(client: TestClient, user: User, # Unflag the package as `user`. post_data = {"action": "unflag", "IDs": [package.ID]} with client as request: - resp = request.post("/packages", data=post_data, cookies=cookies) + request.cookies = cookies + resp = request.post("/packages", data=post_data) assert resp.status_code == int(HTTPStatus.OK) assert package.PackageBase.Flagger is None successes = get_successes(resp.text) @@ -1120,7 +1367,8 @@ def test_packages_post_unflag(client: TestClient, user: User, maint_cookies = {"AURSID": maintainer.login(Request(), "testPassword")} post_data = {"action": "unflag", "IDs": [package.ID]} with client as request: - resp = request.post("/packages", data=post_data, cookies=maint_cookies) + request.cookies = maint_cookies + resp = request.post("/packages", data=post_data) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages to unflag." @@ -1137,8 +1385,8 @@ def test_packages_post_notify(client: TestClient, user: User, package: Package): # an error to be rendered. cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={"action": "notify"}, - cookies=cookies) + request.cookies = cookies + resp = request.post("/packages", data={"action": "notify"}) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages to be notified about." @@ -1146,10 +1394,8 @@ def test_packages_post_notify(client: TestClient, user: User, package: Package): # Now let's actually enable notifications on `package`. with client as request: - resp = request.post("/packages", data={ - "action": "notify", - "IDs": [package.ID] - }, cookies=cookies) + request.cookies = cookies + resp = request.post("/packages", data={"action": "notify", "IDs": [package.ID]}) assert resp.status_code == int(HTTPStatus.OK) expected = "The selected packages' notifications have been enabled." successes = get_successes(resp.text) @@ -1158,31 +1404,27 @@ def test_packages_post_notify(client: TestClient, user: User, package: Package): # Try to enable notifications when they're already enabled, # causing an error to be rendered. with client as request: - resp = request.post("/packages", data={ - "action": "notify", - "IDs": [package.ID] - }, cookies=cookies) + request.cookies = cookies + resp = request.post("/packages", data={"action": "notify", "IDs": [package.ID]}) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages to be notified about." assert errors[0].text.strip() == expected -def test_packages_post_unnotify(client: TestClient, user: User, - package: Package): +def test_packages_post_unnotify(client: TestClient, user: User, package: Package): # Create a notification record. with db.begin(): - notif = db.create(PackageNotification, - PackageBase=package.PackageBase, - User=user) + notif = db.create( + PackageNotification, PackageBase=package.PackageBase, User=user + ) assert notif is not None # Request removal of the notification without any IDs. cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={ - "action": "unnotify" - }, cookies=cookies) + request.cookies = cookies + resp = request.post("/packages", data={"action": "unnotify"}) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages for notification removal." @@ -1190,10 +1432,11 @@ def test_packages_post_unnotify(client: TestClient, user: User, # Request removal of the notification; really. with client as request: - resp = request.post("/packages", data={ - "action": "unnotify", - "IDs": [package.ID] - }, cookies=cookies) + request.cookies = cookies + resp = request.post( + "/packages", + data={"action": "unnotify", "IDs": [package.ID]}, + ) assert resp.status_code == int(HTTPStatus.OK) successes = get_successes(resp.text) expected = "The selected packages' notifications have been removed." @@ -1207,25 +1450,23 @@ def test_packages_post_unnotify(client: TestClient, user: User, # Try it again. The notif no longer exists. with client as request: - resp = request.post("/packages", data={ - "action": "unnotify", - "IDs": [package.ID] - }, cookies=cookies) + request.cookies = cookies + resp = request.post( + "/packages", + data={"action": "unnotify", "IDs": [package.ID]}, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "A package you selected does not have notifications enabled." assert errors[0].text.strip() == expected -def test_packages_post_adopt(client: TestClient, user: User, - package: Package): - +def test_packages_post_adopt(client: TestClient, user: User, package: Package): # Try to adopt an empty list of packages. cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={ - "action": "adopt" - }, cookies=cookies) + request.cookies = cookies + resp = request.post("/packages", data={"action": "adopt"}) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages to adopt." @@ -1233,11 +1474,11 @@ def test_packages_post_adopt(client: TestClient, user: User, # Now, let's try to adopt a package that's already maintained. with client as request: - resp = request.post("/packages", data={ - "action": "adopt", - "IDs": [package.ID], - "confirm": True - }, cookies=cookies) + request.cookies = cookies + resp = request.post( + "/packages", + data={"action": "adopt", "IDs": [package.ID], "confirm": True}, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You are not allowed to adopt one of the packages you selected." @@ -1250,33 +1491,33 @@ def test_packages_post_adopt(client: TestClient, user: User, # Now, let's try to adopt without confirming. with client as request: - resp = request.post("/packages", data={ - "action": "adopt", - "IDs": [package.ID] - }, cookies=cookies) + request.cookies = cookies + resp = request.post("/packages", data={"action": "adopt", "IDs": [package.ID]}) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) - expected = ("The selected packages have not been adopted, " - "check the confirmation checkbox.") + expected = ( + "The selected packages have not been adopted, " + "check the confirmation checkbox." + ) assert errors[0].text.strip() == expected # Let's do it again now that there is no maintainer. with client as request: - resp = request.post("/packages", data={ - "action": "adopt", - "IDs": [package.ID], - "confirm": True - }, cookies=cookies) + request.cookies = cookies + resp = request.post( + "/packages", + data={"action": "adopt", "IDs": [package.ID], "confirm": True}, + ) assert resp.status_code == int(HTTPStatus.OK) successes = get_successes(resp.text) expected = "The selected packages have been adopted." assert successes[0].text.strip() == expected -def test_packages_post_disown_as_maintainer(client: TestClient, user: User, - maintainer: User, - package: Package): - """ Disown packages as a maintainer. """ +def test_packages_post_disown_as_maintainer( + client: TestClient, user: User, maintainer: User, package: Package +): + """Disown packages as a maintainer.""" # Initially prove that we have a maintainer. assert package.PackageBase.Maintainer is not None assert package.PackageBase.Maintainer == maintainer @@ -1284,9 +1525,8 @@ def test_packages_post_disown_as_maintainer(client: TestClient, user: User, # Try to run the disown action with no IDs; get an error. cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={ - "action": "disown" - }, cookies=cookies) + request.cookies = cookies + resp = request.post("/packages", data={"action": "disown"}) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages to disown." @@ -1295,25 +1535,25 @@ def test_packages_post_disown_as_maintainer(client: TestClient, user: User, # Try to disown `package` without giving the confirm argument. with client as request: - resp = request.post("/packages", data={ - "action": "disown", - "IDs": [package.ID] - }, cookies=cookies) + request.cookies = cookies + resp = request.post("/packages", data={"action": "disown", "IDs": [package.ID]}) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) assert package.PackageBase.Maintainer is not None errors = get_errors(resp.text) - expected = ("The selected packages have not been disowned, " - "check the confirmation checkbox.") + expected = ( + "The selected packages have not been disowned, " + "check the confirmation checkbox." + ) assert errors[0].text.strip() == expected # Now, try to disown `package` without credentials (as `user`). user_cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={ - "action": "disown", - "IDs": [package.ID], - "confirm": True - }, cookies=user_cookies) + request.cookies = user_cookies + resp = request.post( + "/packages", + data={"action": "disown", "IDs": [package.ID], "confirm": True}, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) assert package.PackageBase.Maintainer is not None errors = get_errors(resp.text) @@ -1322,11 +1562,11 @@ def test_packages_post_disown_as_maintainer(client: TestClient, user: User, # Now, let's really disown `package` as `maintainer`. with client as request: - resp = request.post("/packages", data={ - "action": "disown", - "IDs": [package.ID], - "confirm": True - }, cookies=cookies) + request.cookies = cookies + resp = request.post( + "/packages", + data={"action": "disown", "IDs": [package.ID], "confirm": True}, + ) assert package.PackageBase.Maintainer is None successes = get_successes(resp.text) @@ -1334,30 +1574,35 @@ def test_packages_post_disown_as_maintainer(client: TestClient, user: User, assert successes[0].text.strip() == expected -def test_packages_post_disown(client: TestClient, tu_user: User, - maintainer: User, package: Package): - """ Disown packages as a Trusted User, which cannot bypass idle time. """ - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} +def test_packages_post_disown( + client: TestClient, pm_user: User, maintainer: User, package: Package +): + """Disown packages as a Package Maintainer, which cannot bypass idle time.""" + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={ - "action": "disown", - "IDs": [package.ID], - "confirm": True - }, cookies=cookies) + request.cookies = cookies + resp = request.post( + "/packages", + data={"action": "disown", "IDs": [package.ID], "confirm": True}, + ) errors = get_errors(resp.text) expected = r"^No due existing orphan requests to accept for .+\.$" assert re.match(expected, errors[0].text.strip()) -def test_packages_post_delete(caplog: pytest.fixture, client: TestClient, - user: User, tu_user: User, package: Package): +def test_packages_post_delete( + caplog: pytest.fixture, + client: TestClient, + user: User, + pm_user: User, + package: Package, +): # First, let's try to use the delete action with no packages IDs. user_cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={ - "action": "delete" - }, cookies=user_cookies) + request.cookies = user_cookies + resp = request.post("/packages", data={"action": "delete"}) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You did not select any packages to delete." @@ -1365,51 +1610,54 @@ def test_packages_post_delete(caplog: pytest.fixture, client: TestClient, # Now, let's try to delete real packages without supplying "confirm". with client as request: - resp = request.post("/packages", data={ - "action": "delete", - "IDs": [package.ID] - }, cookies=user_cookies) + request.cookies = user_cookies + resp = request.post( + "/packages", + data={"action": "delete", "IDs": [package.ID]}, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) - expected = ("The selected packages have not been deleted, " - "check the confirmation checkbox.") + expected = ( + "The selected packages have not been deleted, " + "check the confirmation checkbox." + ) assert errors[0].text.strip() == expected # And again, with everything, but `user` doesn't have permissions. with client as request: - resp = request.post("/packages", data={ - "action": "delete", - "IDs": [package.ID], - "confirm": True - }, cookies=user_cookies) + request.cookies = user_cookies + resp = request.post( + "/packages", + data={"action": "delete", "IDs": [package.ID], "confirm": True}, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "You do not have permission to delete packages." assert errors[0].text.strip() == expected - # Now, let's switch over to making the requests as a TU. + # Now, let's switch over to making the requests as a PM. # However, this next request will be rejected due to supplying # an invalid package ID. - tu_cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + pm_cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={ - "action": "delete", - "IDs": [0], - "confirm": True - }, cookies=tu_cookies) + request.cookies = pm_cookies + resp = request.post( + "/packages", + data={"action": "delete", "IDs": [0], "confirm": True}, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "One of the packages you selected does not exist." assert errors[0].text.strip() == expected - # Whoo. Now, let's finally make a valid request as `tu_user` + # Whoo. Now, let's finally make a valid request as `pm_user` # to delete `package`. with client as request: - resp = request.post("/packages", data={ - "action": "delete", - "IDs": [package.ID], - "confirm": True - }, cookies=tu_cookies) + request.cookies = pm_cookies + resp = request.post( + "/packages", + data={"action": "delete", "IDs": [package.ID], "confirm": True}, + ) assert resp.status_code == int(HTTPStatus.OK) successes = get_successes(resp.text) expected = "The selected packages have been deleted." @@ -1417,44 +1665,53 @@ def test_packages_post_delete(caplog: pytest.fixture, client: TestClient, # Expect that the package deletion was logged. pkgbases = [package.PackageBase.Name] - expected = (f"Privileged user '{tu_user.Username}' deleted the " - f"following package bases: {str(pkgbases)}.") + expected = ( + f"Privileged user '{pm_user.Username}' deleted the " + f"following package bases: {str(pkgbases)}." + ) assert expected in caplog.text def test_account_comments_unauthorized(client: TestClient, user: User): - """ This test may seem out of place, but it requires packages, + """This test may seem out of place, but it requires packages, so its being included in the packages routes test suite to - leverage existing fixtures. """ + leverage existing fixtures.""" endpoint = f"/account/{user.Username}/comments" with client as request: - resp = request.get(endpoint, allow_redirects=False) + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location").startswith("/login") def test_account_comments(client: TestClient, user: User, package: Package): - """ This test may seem out of place, but it requires packages, + """This test may seem out of place, but it requires packages, so its being included in the packages routes test suite to - leverage existing fixtures. """ + leverage existing fixtures.""" now = time.utcnow() with db.begin(): # This comment's CommentTS is `now + 1`, so it is found in rendered # HTML before the rendered_comment, which has a CommentTS of `now`. - comment = db.create(PackageComment, - PackageBase=package.PackageBase, - User=user, Comments="Test comment", - CommentTS=now + 1) - rendered_comment = db.create(PackageComment, - PackageBase=package.PackageBase, - User=user, Comments="Test comment", - RenderedComment="

    Test comment

    ", - CommentTS=now) + comment = db.create( + PackageComment, + PackageBase=package.PackageBase, + User=user, + Comments="Test comment", + CommentTS=now + 1, + ) + rendered_comment = db.create( + PackageComment, + PackageBase=package.PackageBase, + User=user, + Comments="Test comment", + RenderedComment="

    Test comment

    ", + CommentTS=now, + ) cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/account/{user.Username}/comments" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -1464,7 +1721,6 @@ def test_account_comments(client: TestClient, user: User, package: Package): assert comments[0].text.strip() == comment.Comments # And from the second, we have rendered content. - rendered = comments[1].xpath('./p') - expected = rendered_comment.RenderedComment.replace( - "

    ", "").replace("

    ", "") + rendered = comments[1].xpath("./p") + expected = rendered_comment.RenderedComment.replace("

    ", "").replace("

    ", "") assert rendered[0].text.strip() == expected diff --git a/test/test_packages_util.py b/test/test_packages_util.py index 02f84601..649e7a99 100644 --- a/test/test_packages_util.py +++ b/test/test_packages_util.py @@ -1,18 +1,21 @@ import pytest - from fastapi.testclient import TestClient from aurweb import asgi, config, db, time +from aurweb.aur_redis import kill_redis from aurweb.models.account_type import USER_ID +from aurweb.models.dependency_type import DEPENDS_ID from aurweb.models.official_provider import OFFICIAL_BASE, OfficialProvider from aurweb.models.package import Package from aurweb.models.package_base import PackageBase +from aurweb.models.package_dependency import PackageDependency from aurweb.models.package_notification import PackageNotification +from aurweb.models.package_relation import PackageRelation from aurweb.models.package_source import PackageSource from aurweb.models.package_vote import PackageVote +from aurweb.models.relation_type import PROVIDES_ID from aurweb.models.user import User from aurweb.packages import util -from aurweb.redis import kill_redis @pytest.fixture(autouse=True) @@ -23,18 +26,22 @@ def setup(db_test): @pytest.fixture def maintainer() -> User: with db.begin(): - maintainer = db.create(User, Username="test_maintainer", - Email="test_maintainer@examepl.org", - Passwd="testPassword", - AccountTypeID=USER_ID) + maintainer = db.create( + User, + Username="test_maintainer", + Email="test_maintainer@examepl.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield maintainer @pytest.fixture def package(maintainer: User) -> Package: with db.begin(): - pkgbase = db.create(PackageBase, Name="test-pkg", - Packager=maintainer, Maintainer=maintainer) + pkgbase = db.create( + PackageBase, Name="test-pkg", Packager=maintainer, Maintainer=maintainer + ) package = db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase) yield package @@ -51,10 +58,9 @@ def test_package_link(client: TestClient, package: Package): def test_official_package_link(client: TestClient, package: Package): with db.begin(): - provider = db.create(OfficialProvider, - Name=package.Name, - Repo="core", - Provides=package.Name) + provider = db.create( + OfficialProvider, Name=package.Name, Repo="core", Provides=package.Name + ) expected = f"{OFFICIAL_BASE}/packages/?q={package.Name}" assert util.package_link(provider) == expected @@ -63,9 +69,7 @@ def test_updated_packages(maintainer: User, package: Package): expected = { "Name": package.Name, "Version": package.Version, - "PackageBase": { - "ModifiedTS": package.PackageBase.ModifiedTS - } + "PackageBase": {"ModifiedTS": package.PackageBase.ModifiedTS}, } kill_redis() # Kill it here to ensure we're on a fake instance. @@ -77,8 +81,9 @@ def test_updated_packages(maintainer: User, package: Package): def test_query_voted(maintainer: User, package: Package): now = time.utcnow() with db.begin(): - db.create(PackageVote, User=maintainer, VoteTS=now, - PackageBase=package.PackageBase) + db.create( + PackageVote, User=maintainer, VoteTS=now, PackageBase=package.PackageBase + ) query = db.query(Package).filter(Package.ID == package.ID).all() query_voted = util.query_voted(query, maintainer) @@ -87,8 +92,7 @@ def test_query_voted(maintainer: User, package: Package): def test_query_notified(maintainer: User, package: Package): with db.begin(): - db.create(PackageNotification, User=maintainer, - PackageBase=package.PackageBase) + db.create(PackageNotification, User=maintainer, PackageBase=package.PackageBase) query = db.query(Package).filter(Package.ID == package.ID).all() query_notified = util.query_notified(query, maintainer) @@ -99,21 +103,29 @@ def test_source_uri_file(package: Package): FILE = "test_file" with db.begin(): - pkgsrc = db.create(PackageSource, Source=FILE, - Package=package, SourceArch="x86_64") + pkgsrc = db.create( + PackageSource, Source=FILE, Package=package, SourceArch="x86_64" + ) source_file_uri = config.get("options", "source_file_uri") file, uri = util.source_uri(pkgsrc) expected = source_file_uri % (pkgsrc.Source, package.PackageBase.Name) assert (file, uri) == (FILE, expected) + # test URL encoding + pkgsrc.Package.PackageBase.Name = "test++" + file, uri = util.source_uri(pkgsrc) + expected = source_file_uri % (pkgsrc.Source, "test%2B%2B") + assert uri == expected + def test_source_uri_named_uri(package: Package): FILE = "test" URL = "https://test.xyz" with db.begin(): - pkgsrc = db.create(PackageSource, Source=f"{FILE}::{URL}", - Package=package, SourceArch="x86_64") + pkgsrc = db.create( + PackageSource, Source=f"{FILE}::{URL}", Package=package, SourceArch="x86_64" + ) file, uri = util.source_uri(pkgsrc) assert (file, uri) == (FILE, URL) @@ -122,7 +134,64 @@ def test_source_uri_unnamed_uri(package: Package): URL = "https://test.xyz" with db.begin(): - pkgsrc = db.create(PackageSource, Source=f"{URL}", - Package=package, SourceArch="x86_64") + pkgsrc = db.create( + PackageSource, Source=f"{URL}", Package=package, SourceArch="x86_64" + ) file, uri = util.source_uri(pkgsrc) assert (file, uri) == (URL, URL) + + +def test_pkg_required(package: Package): + with db.begin(): + db.create( + PackageDependency, + Package=package, + DepName="test", + DepTypeID=DEPENDS_ID, + ) + + # We want to make sure "Package" data is included + # to avoid lazy-loading the information for each dependency + qry = util.pkg_required("test", list()) + assert "Packages_ID" in str(qry) + + # We should have 1 record + assert qry.count() == 1 + + +def test_provides_markup(package: Package): + # Create dependency and provider for AUR pkg + with db.begin(): + dep = db.create( + PackageDependency, + Package=package, + DepName="test", + DepTypeID=DEPENDS_ID, + ) + rel_pkg = db.create(Package, PackageBase=package.PackageBase, Name=dep.DepName) + db.create( + PackageRelation, + Package=rel_pkg, + RelName=dep.DepName, + RelTypeID=PROVIDES_ID, + ) + + # AUR provider links should end with AUR + link = util.provides_markup(dep.provides()) + assert link.endswith("AUR") + assert OFFICIAL_BASE not in link + + # Remove AUR provider and add official one + with db.begin(): + db.delete(rel_pkg) + db.create( + OfficialProvider, + Name="official-pkg", + Repo="extra", + Provides=dep.DepName, + ) + + # Repo provider links should not have any suffix + link = util.provides_markup(dep.provides()) + assert link.endswith("") + assert OFFICIAL_BASE in link diff --git a/test/test_pkgbase_routes.py b/test/test_pkgbase_routes.py index 5edae592..522bb68b 100644 --- a/test/test_pkgbase_routes.py +++ b/test/test_pkgbase_routes.py @@ -1,15 +1,12 @@ import re - from http import HTTPStatus -from typing import List from unittest import mock import pytest - from fastapi.testclient import TestClient from sqlalchemy import and_ -from aurweb import asgi, db, time +from aurweb import asgi, config, db, time from aurweb.models.account_type import USER_ID, AccountType from aurweb.models.dependency_type import DependencyType from aurweb.models.package import Package @@ -28,36 +25,32 @@ from aurweb.testing.email import Email from aurweb.testing.html import get_errors, get_successes, parse_root from aurweb.testing.requests import Request +max_chars_comment = config.getint("options", "max_chars_comment", 5000) + def package_endpoint(package: Package) -> str: return f"/packages/{package.Name}" def create_package(pkgname: str, maintainer: User) -> Package: - pkgbase = db.create(PackageBase, - Name=pkgname, - Maintainer=maintainer) + pkgbase = db.create(PackageBase, Name=pkgname, Maintainer=maintainer) return db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase) -def create_package_dep(package: Package, depname: str, - dep_type_name: str = "depends") -> PackageDependency: - dep_type = db.query(DependencyType, - DependencyType.Name == dep_type_name).first() - return db.create(PackageDependency, - DependencyType=dep_type, - Package=package, - DepName=depname) +def create_package_dep( + package: Package, depname: str, dep_type_name: str = "depends" +) -> PackageDependency: + dep_type = db.query(DependencyType, DependencyType.Name == dep_type_name).first() + return db.create( + PackageDependency, DependencyType=dep_type, Package=package, DepName=depname + ) -def create_package_rel(package: Package, - relname: str) -> PackageRelation: - rel_type = db.query(RelationType, - RelationType.ID == PROVIDES_ID).first() - return db.create(PackageRelation, - RelationType=rel_type, - Package=package, - RelName=relname) +def create_package_rel(package: Package, relname: str) -> PackageRelation: + rel_type = db.query(RelationType, RelationType.ID == PROVIDES_ID).first() + return db.create( + PackageRelation, RelationType=rel_type, Package=package, RelName=relname + ) @pytest.fixture(autouse=True) @@ -67,64 +60,94 @@ def setup(db_test): @pytest.fixture def client() -> TestClient: - """ Yield a FastAPI TestClient. """ - yield TestClient(app=asgi.app) + """Yield a FastAPI TestClient.""" + client = TestClient(app=asgi.app) + + # disable redirects for our tests + client.follow_redirects = False + yield client def create_user(username: str) -> User: with db.begin(): - user = db.create(User, Username=username, - Email=f"{username}@example.org", - Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username=username, + Email=f"{username}@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) return user @pytest.fixture def user() -> User: - """ Yield a user. """ + """Yield a user.""" user = create_user("test") yield user @pytest.fixture def maintainer() -> User: - """ Yield a specific User used to maintain packages. """ + """Yield a specific User used to maintain packages.""" account_type = db.query(AccountType, AccountType.ID == USER_ID).first() with db.begin(): - maintainer = db.create(User, Username="test_maintainer", - Email="test_maintainer@example.org", - Passwd="testPassword", - AccountType=account_type) + maintainer = db.create( + User, + Username="test_maintainer", + Email="test_maintainer@example.org", + Passwd="testPassword", + AccountType=account_type, + ) yield maintainer @pytest.fixture -def tu_user(): - tu_type = db.query(AccountType, - AccountType.AccountType == "Trusted User").first() +def comaintainer() -> User: + """Yield a specific User used to maintain packages.""" + account_type = db.query(AccountType, AccountType.ID == USER_ID).first() with db.begin(): - tu_user = db.create(User, Username="test_tu", - Email="test_tu@example.org", - RealName="Test TU", Passwd="testPassword", - AccountType=tu_type) - yield tu_user + comaintainer = db.create( + User, + Username="test_comaintainer", + Email="test_comaintainer@example.org", + Passwd="testPassword", + AccountType=account_type, + ) + yield comaintainer + + +@pytest.fixture +def pm_user(): + pm_type = db.query( + AccountType, AccountType.AccountType == "Package Maintainer" + ).first() + with db.begin(): + pm_user = db.create( + User, + Username="test_pm", + Email="test_pm@example.org", + RealName="Test PM", + Passwd="testPassword", + AccountType=pm_type, + ) + yield pm_user @pytest.fixture def package(maintainer: User) -> Package: - """ Yield a Package created by user. """ + """Yield a Package created by user.""" now = time.utcnow() with db.begin(): - pkgbase = db.create(PackageBase, - Name="test-package", - Maintainer=maintainer, - Packager=maintainer, - Submitter=maintainer, - ModifiedTS=now) - package = db.create(Package, - PackageBase=pkgbase, - Name=pkgbase.Name) + pkgbase = db.create( + PackageBase, + Name="test-package", + Maintainer=maintainer, + Packager=maintainer, + Submitter=maintainer, + ModifiedTS=now, + ) + package = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) yield package @@ -135,29 +158,34 @@ def pkgbase(package: Package) -> PackageBase: @pytest.fixture def target(maintainer: User) -> PackageBase: - """ Merge target. """ + """Merge target.""" now = time.utcnow() with db.begin(): - pkgbase = db.create(PackageBase, Name="target-package", - Maintainer=maintainer, - Packager=maintainer, - Submitter=maintainer, - ModifiedTS=now) + pkgbase = db.create( + PackageBase, + Name="target-package", + Maintainer=maintainer, + Packager=maintainer, + Submitter=maintainer, + ModifiedTS=now, + ) db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) yield pkgbase @pytest.fixture def pkgreq(user: User, pkgbase: PackageBase) -> PackageRequest: - """ Yield a PackageRequest related to `pkgbase`. """ + """Yield a PackageRequest related to `pkgbase`.""" with db.begin(): - pkgreq = db.create(PackageRequest, - ReqTypeID=DELETION_ID, - User=user, - PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - Comments=f"Deletion request for {pkgbase.Name}", - ClosureComment=str()) + pkgreq = db.create( + PackageRequest, + ReqTypeID=DELETION_ID, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + Comments=f"Deletion request for {pkgbase.Name}", + ClosureComment=str(), + ) yield pkgreq @@ -166,51 +194,53 @@ def comment(user: User, package: Package) -> PackageComment: pkgbase = package.PackageBase now = time.utcnow() with db.begin(): - comment = db.create(PackageComment, - User=user, - PackageBase=pkgbase, - Comments="Test comment.", - RenderedComment=str(), - CommentTS=now) + comment = db.create( + PackageComment, + User=user, + PackageBase=pkgbase, + Comments="Test comment.", + RenderedComment=str(), + CommentTS=now, + ) yield comment @pytest.fixture -def packages(maintainer: User) -> List[Package]: - """ Yield 55 packages named pkg_0 .. pkg_54. """ +def packages(maintainer: User) -> list[Package]: + """Yield 55 packages named pkg_0 .. pkg_54.""" packages_ = [] now = time.utcnow() with db.begin(): for i in range(55): - pkgbase = db.create(PackageBase, - Name=f"pkg_{i}", - Maintainer=maintainer, - Packager=maintainer, - Submitter=maintainer, - ModifiedTS=now) - package = db.create(Package, - PackageBase=pkgbase, - Name=f"pkg_{i}") + pkgbase = db.create( + PackageBase, + Name=f"pkg_{i}", + Maintainer=maintainer, + Packager=maintainer, + Submitter=maintainer, + ModifiedTS=now, + ) + package = db.create(Package, PackageBase=pkgbase, Name=f"pkg_{i}") packages_.append(package) yield packages_ @pytest.fixture -def requests(user: User, packages: List[Package]) -> List[PackageRequest]: +def requests(user: User, packages: list[Package]) -> list[PackageRequest]: pkgreqs = [] - deletion_type = db.query(RequestType).filter( - RequestType.ID == DELETION_ID - ).first() + deletion_type = db.query(RequestType).filter(RequestType.ID == DELETION_ID).first() with db.begin(): for i in range(55): - pkgreq = db.create(PackageRequest, - RequestType=deletion_type, - User=user, - PackageBase=packages[i].PackageBase, - PackageBaseName=packages[i].Name, - Comments=f"Deletion request for pkg_{i}", - ClosureComment=str()) + pkgreq = db.create( + PackageRequest, + RequestType=deletion_type, + User=user, + PackageBase=packages[i].PackageBase, + PackageBaseName=packages[i].Name, + Comments=f"Deletion request for pkg_{i}", + ClosureComment=str(), + ) pkgreqs.append(pkgreq) yield pkgreqs @@ -223,21 +253,18 @@ def test_pkgbase_not_found(client: TestClient): def test_pkgbase_redirect(client: TestClient, package: Package): with client as request: - resp = request.get(f"/pkgbase/{package.Name}", - allow_redirects=False) + resp = request.get(f"/pkgbase/{package.Name}") assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/packages/{package.Name}" def test_pkgbase(client: TestClient, package: Package): with db.begin(): - second = db.create(Package, Name="second-pkg", - PackageBase=package.PackageBase) + second = db.create(Package, Name="second-pkg", PackageBase=package.PackageBase) expected = [package.Name, second.Name] with client as request: - resp = request.get(f"/pkgbase/{package.Name}", - allow_redirects=False) + resp = request.get(f"/pkgbase/{package.Name}") assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -253,8 +280,9 @@ def test_pkgbase(client: TestClient, package: Package): assert pkgs[i].text.strip() == name -def test_pkgbase_maintainer(client: TestClient, user: User, maintainer: User, - package: Package): +def test_pkgbase_maintainer( + client: TestClient, user: User, maintainer: User, package: Package +): """ Test that the Maintainer field is beind displayed correctly. @@ -262,44 +290,44 @@ def test_pkgbase_maintainer(client: TestClient, user: User, maintainer: User, the maintainer. """ with db.begin(): - db.create(PackageComaintainer, User=user, - PackageBase=package.PackageBase, - Priority=1) + db.create( + PackageComaintainer, User=user, PackageBase=package.PackageBase, Priority=1 + ) with client as request: - resp = request.get(f"/pkgbase/{package.Name}") + resp = request.get(f"/pkgbase/{package.Name}", follow_redirects=True) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) maint = root.xpath('//table[@id="pkginfo"]/tr[@class="pkgmaint"]/td')[0] - maint, comaint = maint.xpath('./a') - assert maint.text.strip() == maintainer.Username - assert comaint.text.strip() == user.Username + maint, comaint = maint.text.strip().split() + assert maint == maintainer.Username + assert comaint == f"({user.Username})" -def test_pkgbase_voters(client: TestClient, tu_user: User, package: Package): +def test_pkgbase_voters(client: TestClient, pm_user: User, package: Package): pkgbase = package.PackageBase endpoint = f"/pkgbase/{pkgbase.Name}/voters" now = time.utcnow() with db.begin(): - db.create(PackageVote, User=tu_user, PackageBase=pkgbase, VoteTS=now) + db.create(PackageVote, User=pm_user, PackageBase=pkgbase, VoteTS=now) - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: - resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) - # We should've gotten one link to the voter, tu_user. + # We should've gotten one link to the voter, pm_user. root = parse_root(resp.text) rows = root.xpath('//div[@class="box"]//ul/li/a') assert len(rows) == 1 - assert rows[0].text.strip() == tu_user.Username + assert rows[0].text.strip() == pm_user.Username -def test_pkgbase_voters_unauthorized(client: TestClient, user: User, - package: Package): +def test_pkgbase_voters_unauthorized(client: TestClient, user: User, package: Package): pkgbase = package.PackageBase endpoint = f"/pkgbase/{pkgbase.Name}/voters" @@ -308,82 +336,111 @@ def test_pkgbase_voters_unauthorized(client: TestClient, user: User, db.create(PackageVote, User=user, PackageBase=pkgbase, VoteTS=now) with client as request: - resp = request.get(endpoint, allow_redirects=False) + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" -def test_pkgbase_comment_not_found(client: TestClient, maintainer: User, - package: Package): +def test_pkgbase_comment_not_found( + client: TestClient, maintainer: User, package: Package +): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} comment_id = 12345 # A non-existing comment. endpoint = f"/pkgbase/{package.PackageBase.Name}/comments/{comment_id}" with client as request: - resp = request.post(endpoint, data={ - "comment": "Failure" - }, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data={"comment": "Failure"}) assert resp.status_code == int(HTTPStatus.NOT_FOUND) -def test_pkgbase_comment_form_unauthorized(client: TestClient, user: User, - maintainer: User, package: Package): +def test_pkgbase_comment_form_unauthorized( + client: TestClient, user: User, maintainer: User, package: Package +): now = time.utcnow() with db.begin(): - comment = db.create(PackageComment, PackageBase=package.PackageBase, - User=maintainer, Comments="Test", - RenderedComment=str(), CommentTS=now) + comment = db.create( + PackageComment, + PackageBase=package.PackageBase, + User=maintainer, + Comments="Test", + RenderedComment=str(), + CommentTS=now, + ) cookies = {"AURSID": user.login(Request(), "testPassword")} pkgbasename = package.PackageBase.Name endpoint = f"/pkgbase/{pkgbasename}/comments/{comment.ID}/form" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) -def test_pkgbase_comment_form_not_found(client: TestClient, maintainer: User, - package: Package): +def test_pkgbase_comment_form_not_found( + client: TestClient, maintainer: User, package: Package +): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} comment_id = 12345 # A non-existing comment. pkgbasename = package.PackageBase.Name endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/form" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.NOT_FOUND) -def test_pkgbase_comments_missing_comment(client: TestClient, maintainer: User, - package: Package): +def test_pkgbase_comments_missing_comment( + client: TestClient, maintainer: User, package: Package +): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/comments" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) -def test_pkgbase_comments(client: TestClient, maintainer: User, user: User, - package: Package): - """ This test includes tests against the following routes: +def test_pkgbase_comments( + client: TestClient, maintainer: User, user: User, package: Package +): + """This test includes tests against the following routes: + - GET /pkgbase/{name} (to check notification checkbox) - POST /pkgbase/{name}/comments - GET /pkgbase/{name} (to check comments) - Tested against a comment created with the POST route - GET /pkgbase/{name}/comments/{id}/form - Tested against a comment created with the POST route """ - with db.begin(): - user.CommentNotify = 1 - db.create(PackageNotification, - PackageBase=package.PackageBase, - User=user) - cookies = {"AURSID": maintainer.login(Request(), "testPassword")} pkgbasename = package.PackageBase.Name + + endpoint = f"/pkgbase/{pkgbasename}" + with client as request: + request.cookies = cookies + resp = request.get( + endpoint, + follow_redirects=True, + ) + assert resp.status_code == int(HTTPStatus.OK) + + # Make sure we got our checkbox for enabling notifications + root = parse_root(resp.text) + input = root.find('//input[@id="id_enable_notifications"]') + assert input is not None + + # create notification + with db.begin(): + user.CommentNotify = 1 + db.create(PackageNotification, PackageBase=package.PackageBase, User=user) + + # post a comment endpoint = f"/pkgbase/{pkgbasename}/comments" with client as request: - resp = request.post(endpoint, data={ - "comment": "Test comment.", - "enable_notifications": True - }, cookies=cookies) + request.cookies = cookies + resp = request.post( + endpoint, + data={"comment": "Test comment.", "enable_notifications": True}, + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # user should've gotten a CommentNotification email. @@ -394,7 +451,7 @@ def test_pkgbase_comments(client: TestClient, maintainer: User, user: User, assert resp.headers.get("location")[:prefix_len] == expected_prefix with client as request: - resp = request.get(resp.headers.get("location")) + resp = request.get(resp.headers.get("location"), follow_redirects=True) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -407,10 +464,16 @@ def test_pkgbase_comments(client: TestClient, maintainer: User, user: User, assert bodies[0].text.strip() == "Test comment." comment_id = headers[0].attrib["id"].split("-")[-1] + # Since we've enabled notifications already, + # there should be no checkbox on our page + input = root.find('//input[@id="id_enable_notifications"]') + assert input is None + # Test the non-javascript version of comment editing by # visiting the /pkgbase/{name}/comments/{id}/edit route. with client as request: - resp = request.get(f"{endpoint}/{comment_id}/edit", cookies=cookies) + request.cookies = cookies + resp = request.get(f"{endpoint}/{comment_id}/edit") assert resp.status_code == int(HTTPStatus.OK) # Clear up the PackageNotification. This doubles as testing @@ -427,14 +490,15 @@ def test_pkgbase_comments(client: TestClient, maintainer: User, user: User, comment_id = int(headers[0].attrib["id"].split("-")[-1]) endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}" with client as request: - resp = request.post(endpoint, data={ - "comment": "Edited comment.", - "enable_notifications": True - }, cookies=cookies) + request.cookies = cookies + resp = request.post( + endpoint, + data={"comment": "Edited comment.", "enable_notifications": True}, + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) with client as request: - resp = request.get(resp.headers.get("location")) + resp = request.get(resp.headers.get("location"), follow_redirects=True) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -452,33 +516,130 @@ def test_pkgbase_comments(client: TestClient, maintainer: User, user: User, ).first() assert db_notif is not None + # Now, let's edit again, but cancel. + endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}" + with client as request: + request.cookies = cookies + resp = request.post( + endpoint, + data={"comment": "Edited comment with cancel.", "cancel": True}, + ) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + with client as request: + resp = request.get(resp.headers.get("location"), follow_redirects=True) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + bodies = root.xpath('//div[@class="article-content"]/div/p') + + # Make sure our comment was NOT changed + assert bodies[0].text.strip() == "Edited comment." + + # Delete notification for next test. + with db.begin(): + db.delete(db_notif) + + # Let's edit the comment again; This time we don't change the text. + # However we do enable notifications. + with client as request: + request.cookies = cookies + resp = request.post( + endpoint, + data={"comment": "Edited comment.", "enable_notifications": True}, + ) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + # Ensure that a notification was created. + db_notif = pkgbase.notifications.filter( + PackageNotification.UserID == maintainer.ID + ).first() + assert db_notif is not None + # Don't supply a comment; should return BAD_REQUEST. with client as request: - fail_resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + fail_resp = request.post(endpoint) assert fail_resp.status_code == int(HTTPStatus.BAD_REQUEST) # Now, test the form route, which should return form markup # via JSON. endpoint = f"{endpoint}/form" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) data = resp.json() assert "form" in data -def test_pkgbase_comment_delete(client: TestClient, - maintainer: User, - user: User, - package: Package, - comment: PackageComment): +def test_pkgbase_comment_exceed_character_limit( + client: TestClient, + user: User, + package: Package, + comment: PackageComment, +): + # Post new comment + cookies = {"AURSID": user.login(Request(), "testPassword")} + pkgbasename = package.PackageBase.Name + endpoint = f"/pkgbase/{pkgbasename}/comments" + + with client as request: + request.cookies = cookies + resp = request.post( + endpoint, + data={"comment": "x" * (max_chars_comment + 1)}, + ) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + assert "Maximum number of characters for comment exceeded." in resp.text + # Edit existing + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + request.cookies = cookies + endp = f"/pkgbase/{pkgbasename}/comments/{comment.ID}" + response = request.post( + endp, + data={"comment": "x" * (max_chars_comment + 1)}, + ) + assert response.status_code == HTTPStatus.BAD_REQUEST + assert "Maximum number of characters for comment exceeded." in resp.text + + +def test_pkgbase_comment_edit_unauthorized( + client: TestClient, + user: User, + maintainer: User, + package: Package, + comment: PackageComment, +): + pkgbase = package.PackageBase + + cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + with client as request: + request.cookies = cookies + endp = f"/pkgbase/{pkgbase.Name}/comments/{comment.ID}" + response = request.post( + endp, + data={"comment": "abcd im trying to change this comment."}, + ) + assert response.status_code == HTTPStatus.UNAUTHORIZED + + +def test_pkgbase_comment_delete( + client: TestClient, + maintainer: User, + user: User, + package: Package, + comment: PackageComment, +): # Test the unauthorized case of comment deletion. cookies = {"AURSID": user.login(Request(), "testPassword")} pkgbasename = package.PackageBase.Name endpoint = f"/pkgbase/{pkgbasename}/comments/{comment.ID}/delete" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) expected = f"/pkgbase/{pkgbasename}" @@ -488,66 +649,76 @@ def test_pkgbase_comment_delete(client: TestClient, maint_cookies = {"AURSID": maintainer.login(Request(), "testPassword")} endpoint = f"/pkgbase/{pkgbasename}/comments/{comment.ID}/undelete" with client as request: - resp = request.post(endpoint, cookies=maint_cookies) + request.cookies = maint_cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) # And move on to undeleting it. with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) -def test_pkgbase_comment_delete_unauthorized(client: TestClient, - maintainer: User, - package: Package, - comment: PackageComment): +def test_pkgbase_comment_delete_unauthorized( + client: TestClient, maintainer: User, package: Package, comment: PackageComment +): # Test the unauthorized case of comment deletion. cookies = {"AURSID": maintainer.login(Request(), "testPassword")} pkgbasename = package.PackageBase.Name endpoint = f"/pkgbase/{pkgbasename}/comments/{comment.ID}/delete" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) -def test_pkgbase_comment_delete_not_found(client: TestClient, - maintainer: User, - package: Package): +def test_pkgbase_comment_delete_not_found( + client: TestClient, maintainer: User, package: Package +): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} comment_id = 12345 # Non-existing comment. pkgbasename = package.PackageBase.Name endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/delete" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.NOT_FOUND) -def test_pkgbase_comment_undelete_not_found(client: TestClient, - maintainer: User, - package: Package): +def test_pkgbase_comment_undelete_not_found( + client: TestClient, maintainer: User, package: Package +): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} comment_id = 12345 # Non-existing comment. pkgbasename = package.PackageBase.Name endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/undelete" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.NOT_FOUND) -def test_pkgbase_comment_pin_as_co(client: TestClient, package: Package, - comment: PackageComment): +def test_pkgbase_comment_pin_as_co( + client: TestClient, package: Package, comment: PackageComment +): comaint = create_user("comaint1") with db.begin(): - db.create(PackageComaintainer, PackageBase=package.PackageBase, - User=comaint, Priority=1) + db.create( + PackageComaintainer, + PackageBase=package.PackageBase, + User=comaint, + Priority=1, + ) # Pin the comment. pkgbasename = package.PackageBase.Name endpoint = f"/pkgbase/{pkgbasename}/comments/{comment.ID}/pin" cookies = {"AURSID": comaint.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Assert that PinnedTS got set. @@ -556,17 +727,17 @@ def test_pkgbase_comment_pin_as_co(client: TestClient, package: Package, # Unpin the comment we just pinned. endpoint = f"/pkgbase/{pkgbasename}/comments/{comment.ID}/unpin" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Let's assert that PinnedTS was unset. assert comment.PinnedTS == 0 -def test_pkgbase_comment_pin(client: TestClient, - maintainer: User, - package: Package, - comment: PackageComment): +def test_pkgbase_comment_pin( + client: TestClient, maintainer: User, package: Package, comment: PackageComment +): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} comment_id = comment.ID pkgbasename = package.PackageBase.Name @@ -574,7 +745,8 @@ def test_pkgbase_comment_pin(client: TestClient, # Pin the comment. endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/pin" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Assert that PinnedTS got set. @@ -583,36 +755,37 @@ def test_pkgbase_comment_pin(client: TestClient, # Unpin the comment we just pinned. endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/unpin" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Let's assert that PinnedTS was unset. assert comment.PinnedTS == 0 -def test_pkgbase_comment_pin_unauthorized(client: TestClient, - user: User, - package: Package, - comment: PackageComment): +def test_pkgbase_comment_pin_unauthorized( + client: TestClient, user: User, package: Package, comment: PackageComment +): cookies = {"AURSID": user.login(Request(), "testPassword")} comment_id = comment.ID pkgbasename = package.PackageBase.Name endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/pin" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) -def test_pkgbase_comment_unpin_unauthorized(client: TestClient, - user: User, - package: Package, - comment: PackageComment): +def test_pkgbase_comment_unpin_unauthorized( + client: TestClient, user: User, package: Package, comment: PackageComment +): cookies = {"AURSID": user.login(Request(), "testPassword")} comment_id = comment.ID pkgbasename = package.PackageBase.Name endpoint = f"/pkgbase/{pkgbasename}/comments/{comment_id}/unpin" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) @@ -620,52 +793,55 @@ def test_pkgbase_comaintainers_not_found(client: TestClient, maintainer: User): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} endpoint = "/pkgbase/fake/comaintainers" with client as request: - resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.NOT_FOUND) -def test_pkgbase_comaintainers_post_not_found(client: TestClient, - maintainer: User): +def test_pkgbase_comaintainers_post_not_found(client: TestClient, maintainer: User): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} endpoint = "/pkgbase/fake/comaintainers" with client as request: - resp = request.post(endpoint, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.NOT_FOUND) -def test_pkgbase_comaintainers_unauthorized(client: TestClient, user: User, - package: Package): +def test_pkgbase_comaintainers_unauthorized( + client: TestClient, user: User, package: Package +): pkgbase = package.PackageBase endpoint = f"/pkgbase/{pkgbase.Name}/comaintainers" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" -def test_pkgbase_comaintainers_post_unauthorized(client: TestClient, - user: User, - package: Package): +def test_pkgbase_comaintainers_post_unauthorized( + client: TestClient, user: User, package: Package +): pkgbase = package.PackageBase endpoint = f"/pkgbase/{pkgbase.Name}/comaintainers" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" -def test_pkgbase_comaintainers_post_invalid_user(client: TestClient, - maintainer: User, - package: Package): +def test_pkgbase_comaintainers_post_invalid_user( + client: TestClient, maintainer: User, package: Package +): pkgbase = package.PackageBase endpoint = f"/pkgbase/{pkgbase.Name}/comaintainers" cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, data={ - "users": "\nfake\n" - }, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.post(endpoint, data={"users": "\nfake\n"}) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -673,8 +849,9 @@ def test_pkgbase_comaintainers_post_invalid_user(client: TestClient, assert error.text.strip() == "Invalid user name: fake" -def test_pkgbase_comaintainers(client: TestClient, user: User, - maintainer: User, package: Package): +def test_pkgbase_comaintainers( + client: TestClient, user: User, maintainer: User, package: Package +): pkgbase = package.PackageBase endpoint = f"/pkgbase/{pkgbase.Name}/comaintainers" cookies = {"AURSID": maintainer.login(Request(), "testPassword")} @@ -682,17 +859,21 @@ def test_pkgbase_comaintainers(client: TestClient, user: User, # Start off by adding user as a comaintainer to package. # The maintainer username given should be ignored. with client as request: - resp = request.post(endpoint, data={ - "users": f"\n{user.Username}\n{maintainer.Username}\n" - }, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.post( + endpoint, + data={"users": f"\n{user.Username}\n{maintainer.Username}\n"}, + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" # Do it again to exercise the last_priority bump path. with client as request: - resp = request.post(endpoint, data={ - "users": f"\n{user.Username}\n{maintainer.Username}\n" - }, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.post( + endpoint, + data={"users": f"\n{user.Username}\n{maintainer.Username}\n"}, + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" @@ -700,7 +881,8 @@ def test_pkgbase_comaintainers(client: TestClient, user: User, # let's perform a GET request to make sure that the backend produces # the user we added in the users textarea. with client as request: - resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -709,14 +891,14 @@ def test_pkgbase_comaintainers(client: TestClient, user: User, # Finish off by removing all the comaintainers. with client as request: - resp = request.post(endpoint, data={ - "users": str() - }, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.post(endpoint, data={"users": str()}) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" with client as request: - resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -730,7 +912,8 @@ def test_pkgbase_request_not_found(client: TestClient, user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.NOT_FOUND) @@ -740,39 +923,44 @@ def test_pkgbase_request(client: TestClient, user: User, package: Package): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) def test_pkgbase_request_post_not_found(client: TestClient, user: User): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/pkgbase/fake/request", data={ - "type": "fake" - }, cookies=cookies) + request.cookies = cookies + resp = request.post("/pkgbase/fake/request", data={"type": "fake"}) assert resp.status_code == int(HTTPStatus.NOT_FOUND) -def test_pkgbase_request_post_invalid_type(client: TestClient, - user: User, - package: Package): +def test_pkgbase_request_post_invalid_type( + client: TestClient, user: User, package: Package +): endpoint = f"/pkgbase/{package.PackageBase.Name}/request" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, data={"type": "fake"}, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data={"type": "fake"}) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) -def test_pkgbase_request_post_no_comment_error(client: TestClient, - user: User, - package: Package): +def test_pkgbase_request_post_no_comment_error( + client: TestClient, user: User, package: Package +): endpoint = f"/pkgbase/{package.PackageBase.Name}/request" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, data={ - "type": "deletion", - "comments": "" # An empty comment field causes an error. - }, cookies=cookies) + request.cookies = cookies + resp = request.post( + endpoint, + data={ + "type": "deletion", + "comments": "", # An empty comment field causes an error. + }, + ) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -781,17 +969,43 @@ def test_pkgbase_request_post_no_comment_error(client: TestClient, assert error.text.strip() == expected -def test_pkgbase_request_post_merge_not_found_error(client: TestClient, - user: User, - package: Package): +def test_pkgbase_request_post_comment_exceed_character_limit( + client: TestClient, user: User, package: Package +): endpoint = f"/pkgbase/{package.PackageBase.Name}/request" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, data={ - "type": "merge", - "merge_into": "fake", # There is no PackageBase.Name "fake" - "comments": "We want to merge this." - }, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.post( + endpoint, + data={ + "type": "deletion", + "comments": "x" * (max_chars_comment + 1), + }, + ) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + error = root.xpath('//ul[@class="errorlist"]/li')[0] + expected = "Maximum number of characters for comment exceeded." + assert error.text.strip() == expected + + +def test_pkgbase_request_post_merge_not_found_error( + client: TestClient, user: User, package: Package +): + endpoint = f"/pkgbase/{package.PackageBase.Name}/request" + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + request.cookies = cookies + resp = request.post( + endpoint, + data={ + "type": "merge", + "merge_into": "fake", # There is no PackageBase.Name "fake" + "comments": "We want to merge this.", + }, + ) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -800,17 +1014,21 @@ def test_pkgbase_request_post_merge_not_found_error(client: TestClient, assert error.text.strip() == expected -def test_pkgbase_request_post_merge_no_merge_into_error(client: TestClient, - user: User, - package: Package): +def test_pkgbase_request_post_merge_no_merge_into_error( + client: TestClient, user: User, package: Package +): endpoint = f"/pkgbase/{package.PackageBase.Name}/request" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, data={ - "type": "merge", - "merge_into": "", # There is no PackageBase.Name "fake" - "comments": "We want to merge this." - }, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.post( + endpoint, + data={ + "type": "merge", + "merge_into": "", # There is no PackageBase.Name "fake" + "comments": "We want to merge this.", + }, + ) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -819,16 +1037,21 @@ def test_pkgbase_request_post_merge_no_merge_into_error(client: TestClient, assert error.text.strip() == expected -def test_pkgbase_request_post_merge_self_error(client: TestClient, user: User, - package: Package): +def test_pkgbase_request_post_merge_self_error( + client: TestClient, user: User, package: Package +): endpoint = f"/pkgbase/{package.PackageBase.Name}/request" cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, data={ - "type": "merge", - "merge_into": package.PackageBase.Name, - "comments": "We want to merge this." - }, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.post( + endpoint, + data={ + "type": "merge", + "merge_into": package.PackageBase.Name, + "comments": "We want to merge this.", + }, + ) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -837,8 +1060,9 @@ def test_pkgbase_request_post_merge_self_error(client: TestClient, user: User, assert error.text.strip() == expected -def test_pkgbase_flag(client: TestClient, user: User, maintainer: User, - package: Package): +def test_pkgbase_flag( + client: TestClient, user: User, maintainer: User, package: Package +): pkgbase = package.PackageBase # We shouldn't have flagged the package yet; assert so. @@ -849,27 +1073,28 @@ def test_pkgbase_flag(client: TestClient, user: User, maintainer: User, # Get the flag page. with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) # Now, let's check the /pkgbase/{name}/flag-comment route. flag_comment_endpoint = f"/pkgbase/{pkgbase.Name}/flag-comment" with client as request: - resp = request.get(flag_comment_endpoint, cookies=cookies, - allow_redirects=False) + request.cookies = cookies + resp = request.get(flag_comment_endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" # Try to flag it without a comment. with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) # Flag it with a valid comment. with client as request: - resp = request.post(endpoint, data={ - "comments": "Test" - }, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data={"comments": "Test"}) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert pkgbase.Flagger == user assert pkgbase.FlaggerComment == "Test" @@ -880,48 +1105,61 @@ def test_pkgbase_flag(client: TestClient, user: User, maintainer: User, # Now, let's check the /pkgbase/{name}/flag-comment route. flag_comment_endpoint = f"/pkgbase/{pkgbase.Name}/flag-comment" with client as request: - resp = request.get(flag_comment_endpoint, cookies=cookies, - allow_redirects=False) + request.cookies = cookies + resp = request.get(flag_comment_endpoint) assert resp.status_code == int(HTTPStatus.OK) # Now try to perform a get; we should be redirected because # it's already flagged. with client as request: - resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) with db.begin(): - user2 = db.create(User, Username="test2", - Email="test2@example.org", - Passwd="testPassword", - AccountType=user.AccountType) + user2 = db.create( + User, + Username="test2", + Email="test2@example.org", + Passwd="testPassword", + AccountType=user.AccountType, + ) # Now, test that the 'user2' user can't unflag it, because they # didn't flag it to begin with. user2_cookies = {"AURSID": user2.login(Request(), "testPassword")} endpoint = f"/pkgbase/{pkgbase.Name}/unflag" with client as request: - resp = request.post(endpoint, cookies=user2_cookies) + request.cookies = user2_cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert pkgbase.Flagger == user # Now, test that the 'maintainer' user can. maint_cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, cookies=maint_cookies) + request.cookies = maint_cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert pkgbase.Flagger is None + # Try flagging with a comment that exceeds our character limit. + with client as request: + request.cookies = cookies + data = {"comments": "x" * (max_chars_comment + 1)} + resp = request.post(f"/pkgbase/{pkgbase.Name}/flag", data=data) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + # Flag it again. with client as request: - resp = request.post(f"/pkgbase/{pkgbase.Name}/flag", data={ - "comments": "Test" - }, cookies=cookies) + request.cookies = cookies + resp = request.post(f"/pkgbase/{pkgbase.Name}/flag", data={"comments": "Test"}) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Now, unflag it for real. with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert pkgbase.Flagger is None @@ -934,16 +1172,18 @@ def test_pkgbase_flag_vcs(client: TestClient, user: User, package: Package): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get(f"/pkgbase/{package.PackageBase.Name}/flag", - cookies=cookies) + request.cookies = cookies + resp = request.get(f"/pkgbase/{package.PackageBase.Name}/flag") assert resp.status_code == int(HTTPStatus.OK) - expected = ("This seems to be a VCS package. Please do " - "not flag it out-of-date if the package " - "version in the AUR does not match the most recent commit. " - "Flagging this package should only be done if the sources " - "moved or changes in the PKGBUILD are required because of " - "recent upstream changes.") + expected = ( + "This seems to be a VCS package. Please do " + "not flag it out-of-date if the package " + "version in the AUR does not match the most recent commit. " + "Flagging this package should only be done if the sources " + "moved or changes in the PKGBUILD are required because of " + "recent upstream changes." + ) assert expected in resp.text @@ -951,32 +1191,28 @@ def test_pkgbase_notify(client: TestClient, user: User, package: Package): pkgbase = package.PackageBase # We have no notif record yet; assert that. - notif = pkgbase.notifications.filter( - PackageNotification.UserID == user.ID - ).first() + notif = pkgbase.notifications.filter(PackageNotification.UserID == user.ID).first() assert notif is None # Enable notifications. cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{pkgbase.Name}/notify" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) - notif = pkgbase.notifications.filter( - PackageNotification.UserID == user.ID - ).first() + notif = pkgbase.notifications.filter(PackageNotification.UserID == user.ID).first() assert notif is not None # Disable notifications. endpoint = f"/pkgbase/{pkgbase.Name}/unnotify" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) - notif = pkgbase.notifications.filter( - PackageNotification.UserID == user.ID - ).first() + notif = pkgbase.notifications.filter(PackageNotification.UserID == user.ID).first() assert notif is None @@ -991,7 +1227,8 @@ def test_pkgbase_vote(client: TestClient, user: User, package: Package): cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{pkgbase.Name}/vote" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) vote = pkgbase.package_votes.filter(PackageVote.UsersID == user.ID).first() @@ -1001,7 +1238,8 @@ def test_pkgbase_vote(client: TestClient, user: User, package: Package): # Remove vote. endpoint = f"/pkgbase/{pkgbase.Name}/unvote" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) vote = pkgbase.package_votes.filter(PackageVote.UsersID == user.ID).first() @@ -1009,61 +1247,128 @@ def test_pkgbase_vote(client: TestClient, user: User, package: Package): assert pkgbase.NumVotes == 0 -def test_pkgbase_disown_as_sole_maintainer(client: TestClient, - maintainer: User, - package: Package): +def test_pkgbase_disown_as_sole_maintainer( + client: TestClient, maintainer: User, package: Package +): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} pkgbase = package.PackageBase endpoint = f"/pkgbase/{pkgbase.Name}/disown" # But we do here. with client as request: - resp = request.post(endpoint, data={"confirm": True}, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data={"confirm": True}) assert resp.status_code == int(HTTPStatus.SEE_OTHER) -def test_pkgbase_disown(client: TestClient, user: User, maintainer: User, - package: Package): - cookies = {"AURSID": maintainer.login(Request(), "testPassword")} - user_cookies = {"AURSID": user.login(Request(), "testPassword")} +def test_pkgbase_disown_as_maint_with_comaint( + client: TestClient, user: User, maintainer: User, package: Package +): + """When disowning as a maintainer, the lowest priority comaintainer + is promoted to maintainer.""" pkgbase = package.PackageBase - endpoint = f"/pkgbase/{pkgbase.Name}/disown" + endp = f"/pkgbase/{pkgbase.Name}/disown" + post_data = {"confirm": True} with db.begin(): - db.create(PackageComaintainer, - User=user, - PackageBase=pkgbase, - Priority=1) + db.create(PackageComaintainer, PackageBase=pkgbase, User=user, Priority=1) + + maint_cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + with client as request: + request.cookies = maint_cookies + resp = request.post(endp, data=post_data, follow_redirects=True) + assert resp.status_code == int(HTTPStatus.OK) + + package = db.refresh(package) + pkgbase = package.PackageBase + + assert pkgbase.Maintainer == user + assert pkgbase.comaintainers.count() == 0 + + +def test_pkgbase_disown( + client: TestClient, + user: User, + maintainer: User, + comaintainer: User, + package: Package, +): + maint_cookies = {"AURSID": maintainer.login(Request(), "testPassword")} + comaint_cookies = {"AURSID": comaintainer.login(Request(), "testPassword")} + user_cookies = {"AURSID": user.login(Request(), "testPassword")} + pkgbase = package.PackageBase + pkgbase_endp = f"/pkgbase/{pkgbase.Name}" + endpoint = f"{pkgbase_endp}/disown" + + with db.begin(): + db.create( + PackageComaintainer, User=comaintainer, PackageBase=pkgbase, Priority=1 + ) # GET as a normal user, which is rejected for lack of credentials. with client as request: - resp = request.get(endpoint, cookies=user_cookies, - allow_redirects=False) + request.cookies = user_cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) + # GET as a comaintainer. + with client as request: + request.cookies = comaint_cookies + resp = request.get(endpoint) + assert resp.status_code == int(HTTPStatus.OK) + + # Ensure that the comaintainer can see "Disown Package" link + with client as request: + request.cookies = comaint_cookies + resp = request.get(pkgbase_endp, follow_redirects=True) + assert "Disown Package" in resp.text + # GET as the maintainer. with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = maint_cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) + # Ensure that the maintainer can see "Disown Package" link + with client as request: + request.cookies = maint_cookies + resp = request.get(pkgbase_endp, follow_redirects=True) + assert "Disown Package" in resp.text + # POST as a normal user, which is rejected for lack of credentials. with client as request: - resp = request.post(endpoint, cookies=user_cookies) + request.cookies = user_cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) + # POST as the comaintainer without "confirm". + with client as request: + request.cookies = comaint_cookies + resp = request.post(endpoint) + assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + # POST as the maintainer without "confirm". with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = maint_cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) + # POST as the comaintainer with "confirm". + with client as request: + request.cookies = comaint_cookies + resp = request.post(endpoint, data={"confirm": True}) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + # POST as the maintainer with "confirm". with client as request: - resp = request.post(endpoint, data={"confirm": True}, cookies=cookies) + request.cookies = maint_cookies + resp = request.post(endpoint, data={"confirm": True}) assert resp.status_code == int(HTTPStatus.SEE_OTHER) -def test_pkgbase_adopt(client: TestClient, user: User, tu_user: User, - maintainer: User, package: Package): +def test_pkgbase_adopt( + client: TestClient, user: User, pm_user: User, maintainer: User, package: Package +): # Unset the maintainer as if package is orphaned. with db.begin(): package.PackageBase.Maintainer = None @@ -1074,70 +1379,73 @@ def test_pkgbase_adopt(client: TestClient, user: User, tu_user: User, # Adopt the package base. with client as request: - resp = request.post(endpoint, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert package.PackageBase.Maintainer == maintainer # Try to adopt it when it already has a maintainer; nothing changes. user_cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, cookies=user_cookies, - allow_redirects=False) + request.cookies = user_cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert package.PackageBase.Maintainer == maintainer - # Steal the package as a TU. - tu_cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + # Steal the package as a PM. + pm_cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: - resp = request.post(endpoint, cookies=tu_cookies, - allow_redirects=False) + request.cookies = pm_cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) - assert package.PackageBase.Maintainer == tu_user + assert package.PackageBase.Maintainer == pm_user -def test_pkgbase_delete_unauthorized(client: TestClient, user: User, - package: Package): +def test_pkgbase_delete_unauthorized(client: TestClient, user: User, package: Package): pkgbase = package.PackageBase cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{pkgbase.Name}/delete" # Test GET. with client as request: - resp = request.get(endpoint, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" # Test POST. with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" -def test_pkgbase_delete(client: TestClient, tu_user: User, package: Package): +def test_pkgbase_delete(client: TestClient, pm_user: User, package: Package): pkgbase = package.PackageBase # Test that the GET request works. - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{pkgbase.Name}/delete" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) # Test that POST works and denies us because we haven't confirmed. with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) # Test that we can actually delete the pkgbase. with client as request: - resp = request.post(endpoint, data={"confirm": True}, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data={"confirm": True}) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Let's assert that the package base record got removed. - record = db.query(PackageBase).filter( - PackageBase.Name == pkgbase.Name - ).first() + record = db.query(PackageBase).filter(PackageBase.Name == pkgbase.Name).first() assert record is None # Two emails should've been sent out; an autogenerated @@ -1150,17 +1458,18 @@ def test_pkgbase_delete(client: TestClient, tu_user: User, package: Package): assert re.match(expr, subject) -def test_pkgbase_delete_with_request(client: TestClient, tu_user: User, - pkgbase: PackageBase, - pkgreq: PackageRequest): +def test_pkgbase_delete_with_request( + client: TestClient, pm_user: User, pkgbase: PackageBase, pkgreq: PackageRequest +): # TODO: Test that a previously existing request gets Accepted when - # a TU deleted the package. + # a PM deleted the package. - # Delete the package as `tu_user` via POST request. - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + # Delete the package as `pm_user` via POST request. + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{pkgbase.Name}/delete" with client as request: - resp = request.post(endpoint, data={"confirm": True}, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data={"confirm": True}) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == "/packages" @@ -1173,27 +1482,30 @@ def test_pkgbase_delete_with_request(client: TestClient, tu_user: User, assert re.match(expr, email.headers.get("Subject")) -def test_packages_post_unknown_action(client: TestClient, user: User, - package: Package): - +def test_packages_post_unknown_action(client: TestClient, user: User, package: Package): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={"action": "unknown"}, - cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.post( + "/packages", + data={"action": "unknown"}, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) def test_packages_post_error(client: TestClient, user: User, package: Package): - async def stub_action(request: Request, **kwargs): - return (False, ["Some error."]) + return False, ["Some error."] actions = {"stub": stub_action} with mock.patch.dict("aurweb.routers.packages.PACKAGE_ACTIONS", actions): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={"action": "stub"}, - cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.post( + "/packages", + data={"action": "stub"}, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) @@ -1202,16 +1514,18 @@ def test_packages_post_error(client: TestClient, user: User, package: Package): def test_packages_post(client: TestClient, user: User, package: Package): - async def stub_action(request: Request, **kwargs): - return (True, ["Some success."]) + return True, ["Some success."] actions = {"stub": stub_action} with mock.patch.dict("aurweb.routers.packages.PACKAGE_ACTIONS", actions): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post("/packages", data={"action": "stub"}, - cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.post( + "/packages", + data={"action": "stub"}, + ) assert resp.status_code == int(HTTPStatus.OK) errors = get_successes(resp.text) @@ -1219,81 +1533,92 @@ def test_packages_post(client: TestClient, user: User, package: Package): assert errors[0].text.strip() == expected -def test_pkgbase_merge_unauthorized(client: TestClient, user: User, - package: Package): +def test_pkgbase_merge_unauthorized(client: TestClient, user: User, package: Package): cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) -def test_pkgbase_merge(client: TestClient, tu_user: User, package: Package): - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} +def test_pkgbase_merge(client: TestClient, pm_user: User, package: Package): + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" with client as request: - resp = request.get(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) assert not get_errors(resp.text) -def test_pkgbase_merge_post_unauthorized(client: TestClient, user: User, - package: Package): +def test_pkgbase_merge_post_unauthorized( + client: TestClient, user: User, package: Package +): cookies = {"AURSID": user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.UNAUTHORIZED) -def test_pkgbase_merge_post_unconfirmed(client: TestClient, tu_user: User, - package: Package): - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} +def test_pkgbase_merge_post_unconfirmed( + client: TestClient, pm_user: User, package: Package +): + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) - expected = ("The selected packages have not been deleted, " - "check the confirmation checkbox.") + expected = ( + "The selected packages have not been deleted, " + "check the confirmation checkbox." + ) assert errors[0].text.strip() == expected -def test_pkgbase_merge_post_invalid_into(client: TestClient, tu_user: User, - package: Package): - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} +def test_pkgbase_merge_post_invalid_into( + client: TestClient, pm_user: User, package: Package +): + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" with client as request: - resp = request.post(endpoint, data={ - "into": "not_real", - "confirm": True - }, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data={"into": "not_real", "confirm": True}) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "Cannot find package to merge votes and comments into." assert errors[0].text.strip() == expected -def test_pkgbase_merge_post_self_invalid(client: TestClient, tu_user: User, - package: Package): - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} +def test_pkgbase_merge_post_self_invalid( + client: TestClient, pm_user: User, package: Package +): + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" with client as request: - resp = request.post(endpoint, data={ - "into": package.PackageBase.Name, - "confirm": True - }, cookies=cookies) + request.cookies = cookies + resp = request.post( + endpoint, + data={"into": package.PackageBase.Name, "confirm": True}, + ) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) expected = "Cannot merge a package base with itself." assert errors[0].text.strip() == expected -def test_pkgbase_merge_post(client: TestClient, tu_user: User, - package: Package, - pkgbase: PackageBase, - target: PackageBase, - pkgreq: PackageRequest): +def test_pkgbase_merge_post( + client: TestClient, + pm_user: User, + package: Package, + pkgbase: PackageBase, + target: PackageBase, + pkgreq: PackageRequest, +): pkgname = package.Name pkgbasename = pkgbase.Name @@ -1305,24 +1630,28 @@ def test_pkgbase_merge_post(client: TestClient, tu_user: User, pkgreq.MergeBaseName = target.Name # Vote for the package. - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} endpoint = f"/pkgbase/{package.PackageBase.Name}/vote" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Enable notifications. endpoint = f"/pkgbase/{package.PackageBase.Name}/notify" with client as request: - resp = request.post(endpoint, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Comment on the package. endpoint = f"/pkgbase/{package.PackageBase.Name}/comments" with client as request: - resp = request.post(endpoint, data={ - "comment": "Test comment." - }, cookies=cookies) + request.cookies = cookies + resp = request.post( + endpoint, + data={"comment": "Test comment."}, + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Save these relationships for later comparison. @@ -1333,10 +1662,8 @@ def test_pkgbase_merge_post(client: TestClient, tu_user: User, # Merge the package into target. endpoint = f"/pkgbase/{package.PackageBase.Name}/merge" with client as request: - resp = request.post(endpoint, data={ - "into": target.Name, - "confirm": True - }, cookies=cookies) + request.cookies = cookies + resp = request.post(endpoint, data={"into": target.Name, "confirm": True}) assert resp.status_code == int(HTTPStatus.SEE_OTHER) loc = resp.headers.get("location") assert loc == f"/pkgbase/{target.Name}" @@ -1361,34 +1688,44 @@ def test_pkgbase_merge_post(client: TestClient, tu_user: User, assert pkgreq.Closer is not None # A PackageRequest is always created when merging this way. - pkgreq = db.query(PackageRequest).filter( - and_(PackageRequest.ReqTypeID == MERGE_ID, - PackageRequest.PackageBaseName == pkgbasename, - PackageRequest.MergeBaseName == target.Name) - ).first() + pkgreq = ( + db.query(PackageRequest) + .filter( + and_( + PackageRequest.ReqTypeID == MERGE_ID, + PackageRequest.PackageBaseName == pkgbasename, + PackageRequest.MergeBaseName == target.Name, + ) + ) + .first() + ) assert pkgreq is not None def test_pkgbase_keywords(client: TestClient, user: User, package: Package): endpoint = f"/pkgbase/{package.PackageBase.Name}" with client as request: - resp = request.get(endpoint) + resp = request.get(endpoint, follow_redirects=True) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) keywords = root.xpath('//a[@class="keyword"]') assert len(keywords) == 0 - cookies = {"AURSID": user.login(Request(), "testPassword")} + maint = package.PackageBase.Maintainer + cookies = {"AURSID": maint.login(Request(), "testPassword")} post_endpoint = f"{endpoint}/keywords" with client as request: - resp = request.post(post_endpoint, data={ - "keywords": "abc test" - }, cookies=cookies) + request.cookies = cookies + resp = request.post( + post_endpoint, + data={"keywords": "abc test"}, + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) with client as request: - resp = request.get(resp.headers.get("location")) + request.cookies = {} + resp = request.get(resp.headers.get("location"), follow_redirects=True) assert resp.status_code == int(HTTPStatus.OK) root = parse_root(resp.text) @@ -1397,3 +1734,110 @@ def test_pkgbase_keywords(client: TestClient, user: User, package: Package): expected = ["abc", "test"] for i, keyword in enumerate(keywords): assert keyword.text.strip() == expected[i] + + +def test_pkgbase_empty_keywords(client: TestClient, user: User, package: Package): + endpoint = f"/pkgbase/{package.PackageBase.Name}" + with client as request: + request.cookies = {} + resp = request.get(endpoint, follow_redirects=True) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + keywords = root.xpath('//a[@class="keyword"]') + assert len(keywords) == 0 + + maint = package.PackageBase.Maintainer + cookies = {"AURSID": maint.login(Request(), "testPassword")} + post_endpoint = f"{endpoint}/keywords" + with client as request: + request.cookies = cookies + resp = request.post( + post_endpoint, + data={"keywords": "abc test foo bar "}, + ) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + with client as request: + request.cookies = {} + resp = request.get(resp.headers.get("location"), follow_redirects=True) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + keywords = root.xpath('//a[@class="keyword"]') + assert len(keywords) == 4 + expected = ["abc", "bar", "foo", "test"] + for i, keyword in enumerate(keywords): + assert keyword.text.strip() == expected[i] + + +def test_unauthorized_pkgbase_keywords(client: TestClient, package: Package): + with db.begin(): + user = db.create( + User, Username="random_user", Email="random_user", Passwd="testPassword" + ) + + cookies = {"AURSID": user.login(Request(), "testPassword")} + with client as request: + request.cookies = cookies + pkgbase = package.PackageBase + endp = f"/pkgbase/{pkgbase.Name}/keywords" + response = request.post(endp) + assert response.status_code == HTTPStatus.UNAUTHORIZED + + +def test_independent_user_unflag(client: TestClient, user: User, package: Package): + with db.begin(): + flagger = db.create( + User, + Username="test_flagger", + Email="test_flagger@example.com", + Passwd="testPassword", + ) + + pkgbase = package.PackageBase + cookies = {"AURSID": flagger.login(Request(), "testPassword")} + with client as request: + request.cookies = cookies + endp = f"/pkgbase/{pkgbase.Name}/flag" + response = request.post( + endp, + data={"comments": "This thing needs a flag!"}, + follow_redirects=True, + ) + assert response.status_code == HTTPStatus.OK + + # At this point, we've flagged it as `flagger`. + # Now, we should be able to view the "Unflag package" link on the package + # page when browsing as that `flagger` user. + with client as request: + endp = f"/pkgbase/{pkgbase.Name}" + request.cookies = cookies + response = request.get(endp, follow_redirects=True) + assert response.status_code == HTTPStatus.OK + + # Assert that the "Unflag package" link appears in the DOM. + root = parse_root(response.text) + elems = root.xpath('//input[@name="do_UnFlag"]') + assert len(elems) == 1 + + # Now, unflag the package by "clicking" the "Unflag package" link. + with client as request: + endp = f"/pkgbase/{pkgbase.Name}/unflag" + request.cookies = cookies + response = request.post(endp, follow_redirects=True) + assert response.status_code == HTTPStatus.OK + + # For the last time, let's check the GET response. The package should + # not show as flagged anymore, and thus the "Unflag package" link + # should be missing. + with client as request: + endp = f"/pkgbase/{pkgbase.Name}" + request.cookies = cookies + response = request.get(endp, follow_redirects=True) + assert response.status_code == HTTPStatus.OK + + # Assert that the "Unflag package" link does not appear in the DOM. + root = parse_root(response.text) + elems = root.xpath('//input[@name="do_UnFlag"]') + assert len(elems) == 0 diff --git a/test/test_pkgmaint.py b/test/test_pkgmaint.py index 5d6a56de..e427b664 100644 --- a/test/test_pkgmaint.py +++ b/test/test_pkgmaint.py @@ -1,5 +1,3 @@ -from typing import List - import pytest from aurweb import db, time @@ -16,41 +14,47 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - Passwd="testPassword", AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @pytest.fixture -def packages(user: User) -> List[Package]: +def packages(user: User) -> list[Package]: output = [] now = time.utcnow() with db.begin(): for i in range(5): - pkgbase = db.create(PackageBase, Name=f"pkg_{i}", - SubmittedTS=now, - ModifiedTS=now) - pkg = db.create(Package, PackageBase=pkgbase, - Name=f"pkg_{i}", Version=f"{i}.0") + pkgbase = db.create( + PackageBase, Name=f"pkg_{i}", SubmittedTS=now, ModifiedTS=now + ) + pkg = db.create( + Package, PackageBase=pkgbase, Name=f"pkg_{i}", Version=f"{i}.0" + ) output.append(pkg) yield output -def test_pkgmaint_noop(packages: List[Package]): +def test_pkgmaint_noop(packages: list[Package]): assert len(packages) == 5 pkgmaint.main() packages = db.query(Package).all() assert len(packages) == 5 -def test_pkgmaint(packages: List[Package]): +def test_pkgmaint(packages: list[Package]): assert len(packages) == 5 # Modify the first package so it's out of date and gets deleted. with db.begin(): # Reduce SubmittedTS by a day + 10 seconds. - packages[0].PackageBase.SubmittedTS -= (86400 + 10) + packages[0].PackageBase.SubmittedTS -= 86400 + 10 # Run pkgmaint. pkgmaint.main() @@ -58,7 +62,12 @@ def test_pkgmaint(packages: List[Package]): # Query package objects again and assert that the # first package was deleted but all others are intact. packages = db.query(Package).all() - assert len(packages) == 4 - expected = ["pkg_1", "pkg_2", "pkg_3", "pkg_4"] - for i, pkgname in enumerate(expected): - assert packages[i].Name == pkgname + + # !Cleanup of packages without last packager deactivated. + # We should still have 5 packages + assert len(packages) == 5 + + # assert len(packages) == 4 + # expected = ["pkg_1", "pkg_2", "pkg_3", "pkg_4"] + # for i, pkgname in enumerate(expected): + # assert packages[i].Name == pkgname diff --git a/test/test_pm_vote.py b/test/test_pm_vote.py new file mode 100644 index 00000000..54d5c137 --- /dev/null +++ b/test/test_pm_vote.py @@ -0,0 +1,63 @@ +import pytest +from sqlalchemy.exc import IntegrityError + +from aurweb import db, time +from aurweb.models.account_type import PACKAGE_MAINTAINER_ID +from aurweb.models.user import User +from aurweb.models.vote import Vote +from aurweb.models.voteinfo import VoteInfo + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +@pytest.fixture +def user() -> User: + with db.begin(): + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=PACKAGE_MAINTAINER_ID, + ) + yield user + + +@pytest.fixture +def voteinfo(user: User) -> VoteInfo: + ts = time.utcnow() + with db.begin(): + voteinfo = db.create( + VoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=ts, + End=ts + 5, + Quorum=0.5, + Submitter=user, + ) + yield voteinfo + + +def test_vote_creation(user: User, voteinfo: VoteInfo): + with db.begin(): + vote = db.create(Vote, User=user, VoteInfo=voteinfo) + + assert vote.VoteInfo == voteinfo + assert vote.User == user + assert vote in user.votes + assert vote in voteinfo.votes + + +def test_vote_null_user_raises_exception(voteinfo: VoteInfo): + with pytest.raises(IntegrityError): + Vote(VoteInfo=voteinfo) + + +def test_vote_null_voteinfo_raises_exception(user: User): + with pytest.raises(IntegrityError): + Vote(User=user) diff --git a/test/test_ratelimit.py b/test/test_ratelimit.py index 859adea9..ab940773 100644 --- a/test/test_ratelimit.py +++ b/test/test_ratelimit.py @@ -1,16 +1,15 @@ from unittest import mock import pytest - from redis.client import Pipeline -from aurweb import config, db, logging +from aurweb import aur_logging, config, db +from aurweb.aur_redis import redis_connection from aurweb.models import ApiRateLimit from aurweb.ratelimit import check_ratelimit -from aurweb.redis import redis_connection from aurweb.testing.requests import Request -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) @pytest.fixture(autouse=True) @@ -49,6 +48,7 @@ def mock_config_getboolean(return_value: int = 0): if section == "ratelimit" and key == "cache": return return_value return config_getboolean(section, key) + return fn @@ -60,17 +60,22 @@ def mock_config_get(return_value: str = "none"): if section == "options" and key == "cache": return return_value return config_get(section, key) + return fn @mock.patch("aurweb.config.getint", side_effect=mock_config_getint) @mock.patch("aurweb.config.getboolean", side_effect=mock_config_getboolean(1)) @mock.patch("aurweb.config.get", side_effect=mock_config_get("none")) -def test_ratelimit_redis(get: mock.MagicMock, getboolean: mock.MagicMock, - getint: mock.MagicMock, pipeline: Pipeline): - """ This test will only cover aurweb.ratelimit's Redis +def test_ratelimit_redis( + get: mock.MagicMock, + getboolean: mock.MagicMock, + getint: mock.MagicMock, + pipeline: Pipeline, +): + """This test will only cover aurweb.ratelimit's Redis path if a real Redis server is configured. Otherwise, - it'll use the database. """ + it'll use the database.""" # We'll need a Request for everything here. request = Request() @@ -96,9 +101,12 @@ def test_ratelimit_redis(get: mock.MagicMock, getboolean: mock.MagicMock, @mock.patch("aurweb.config.getint", side_effect=mock_config_getint) @mock.patch("aurweb.config.getboolean", side_effect=mock_config_getboolean(0)) @mock.patch("aurweb.config.get", side_effect=mock_config_get("none")) -def test_ratelimit_db(get: mock.MagicMock, getboolean: mock.MagicMock, - getint: mock.MagicMock, pipeline: Pipeline): - +def test_ratelimit_db( + get: mock.MagicMock, + getboolean: mock.MagicMock, + getint: mock.MagicMock, + pipeline: Pipeline, +): # We'll need a Request for everything here. request = Request() diff --git a/test/test_redis.py b/test/test_redis.py index 82aebb57..6f9bdb40 100644 --- a/test/test_redis.py +++ b/test/test_redis.py @@ -3,13 +3,13 @@ from unittest import mock import pytest import aurweb.config - -from aurweb.redis import redis_connection +from aurweb.aur_redis import redis_connection @pytest.fixture -def rediss(): - """ Create a RedisStub. """ +def redis(): + """Create a RedisStub.""" + def mock_get(section, key): return "none" @@ -21,20 +21,20 @@ def rediss(): yield redis -def test_redis_stub(rediss): +def test_redis_stub(redis): # We don't yet have a test key set. - assert rediss.get("test") is None + assert redis.get("test") is None # Set the test key to abc. - rediss.set("test", "abc") - assert rediss.get("test").decode() == "abc" + redis.set("test", "abc") + assert redis.get("test").decode() == "abc" # Test expire. - rediss.expire("test", 0) - assert rediss.get("test") is None + redis.expire("test", 0) + assert redis.get("test") is None # Now, set the test key again and use delete() on it. - rediss.set("test", "abc") - assert rediss.get("test").decode() == "abc" - rediss.delete("test") - assert rediss.get("test") is None + redis.set("test", "abc") + assert redis.get("test").decode() == "abc" + redis.delete("test") + assert redis.get("test") is None diff --git a/test/test_rendercomment.py b/test/test_rendercomment.py index bf4009fd..9d45fea9 100644 --- a/test/test_rendercomment.py +++ b/test/test_rendercomment.py @@ -1,15 +1,17 @@ +import os from unittest import mock +import pygit2 import pytest -from aurweb import config, db, logging, time +from aurweb import aur_logging, config, db, time from aurweb.models import Package, PackageBase, PackageComment, User from aurweb.models.account_type import USER_ID from aurweb.scripts import rendercomment from aurweb.scripts.rendercomment import update_comment_render from aurweb.testing.git import GitRepository -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) aur_location = config.get("options", "aur_location") @@ -31,8 +33,13 @@ def setup(db_test, git: GitRepository): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - Passwd=str(), AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + Passwd=str(), + AccountTypeID=USER_ID, + ) yield user @@ -40,24 +47,32 @@ def user() -> User: def pkgbase(user: User) -> PackageBase: now = time.utcnow() with db.begin(): - pkgbase = db.create(PackageBase, Packager=user, Name="pkgbase_0", - SubmittedTS=now, ModifiedTS=now) + pkgbase = db.create( + PackageBase, + Packager=user, + Name="pkgbase_0", + SubmittedTS=now, + ModifiedTS=now, + ) yield pkgbase @pytest.fixture def package(pkgbase: PackageBase) -> Package: with db.begin(): - package = db.create(Package, PackageBase=pkgbase, - Name=pkgbase.Name, Version="1.0") + package = db.create( + Package, PackageBase=pkgbase, Name=pkgbase.Name, Version="1.0" + ) yield package -def create_comment(user: User, pkgbase: PackageBase, comments: str, - render: bool = True): +def create_comment( + user: User, pkgbase: PackageBase, comments: str, render: bool = True +): with db.begin(): - comment = db.create(PackageComment, User=user, - PackageBase=pkgbase, Comments=comments) + comment = db.create( + PackageComment, User=user, PackageBase=pkgbase, Comments=comments + ) if render: update_comment_render(comment) return comment @@ -86,8 +101,30 @@ def test_rendercomment_main(user: User, pkgbase: PackageBase): def test_markdown_conversion(user: User, pkgbase: PackageBase): text = "*Hello* [world](https://aur.archlinux.org)!" comment = create_comment(user, pkgbase, text) - expected = ('

    Hello ' - 'world!

    ') + expected = "

    Hello " 'world!

    ' + assert comment.RenderedComment == expected + + +def test_markdown_in_html_block(user: User, pkgbase: PackageBase): + # without "markdown" attribute + text = "
    test*Hello*
    " + comment = create_comment(user, pkgbase, text) + expected = "
    test*Hello*
    " + assert comment.RenderedComment == expected + + # with "markdown" attribute + text = "
    test*Hello*
    " + comment = create_comment(user, pkgbase, text) + expected = ( + "
    \n

    testHello

    \n
    " + ) + assert comment.RenderedComment == expected + + +def test_markdown_strikethrough(user: User, pkgbase: PackageBase): + text = "*~~Hello~~world*~~!~~" + comment = create_comment(user, pkgbase, text) + expected = "

    Helloworld!

    " assert comment.RenderedComment == expected @@ -109,7 +146,7 @@ Visit [Arch Linux][arch]. [arch]: https://www.archlinux.org/\ """ comment = create_comment(user, pkgbase, text) - expected = '''\ + expected = """\

    Visit \ https://www.archlinux.org/#_test_. Visit https://www.archlinux.org/. @@ -117,7 +154,7 @@ Visit https://www.archlinux.org/. Visit https://www.archlinux.org/. Visit Arch Linux. Visit Arch Linux.

    \ -''' +""" assert comment.RenderedComment == expected @@ -154,6 +191,43 @@ http://example.com/{commit_hash}\ assert comment.RenderedComment == expected +def test_git_commit_link_multiple_oids( + git: GitRepository, user: User, package: Package +): + # Make sure we get reproducible hashes by hardcoding the dates + date = "Sun, 16 Jul 2023 06:06:06 +0200" + os.environ["GIT_COMMITTER_DATE"] = date + os.environ["GIT_AUTHOR_DATE"] = date + + # Package names that cause two object IDs starting with "09a3468" + pkgnames = [ + "bfa3e330-23c5-11ee-9b6c-9c2dcdf2810d", + "54c6a420-23c6-11ee-9b6c-9c2dcdf2810d", + ] + + # Create our commits + for pkgname in pkgnames: + with db.begin(): + package = db.create( + Package, PackageBase=package.PackageBase, Name=pkgname, Version="1.0" + ) + git.commit(package, pkgname) + + repo_path = config.get("serve", "repo-path") + repo = pygit2.Repository(repo_path) + + # Make sure we get an error when we search the git repo for "09a3468" + with pytest.raises(ValueError) as oid_error: + assert "09a3468" in repo + assert "ambiguous OID prefix" in oid_error + + # Create a comment, referencing "09a3468" + comment = create_comment(user, package.PackageBase, "Commit 09a3468 is nasty!") + + # Make sure our comment does not contain a link. + assert comment.RenderedComment == "

    Commit 09a3468 is nasty!

    " + + def test_flyspray_issue_link(user: User, pkgbase: PackageBase): text = """\ FS#1234567. diff --git a/test/test_requests.py b/test/test_requests.py index 5ac558e0..1e9cac65 100644 --- a/test/test_requests.py +++ b/test/test_requests.py @@ -1,17 +1,14 @@ import re - from http import HTTPStatus from logging import DEBUG -from typing import List import pytest - from fastapi import HTTPException from fastapi.testclient import TestClient from aurweb import asgi, config, db, defaults, time from aurweb.models import Package, PackageBase, PackageRequest, User -from aurweb.models.account_type import TRUSTED_USER_ID, USER_ID +from aurweb.models.account_type import PACKAGE_MAINTAINER_ID, USER_ID from aurweb.models.package_comaintainer import PackageComaintainer from aurweb.models.package_notification import PackageNotification from aurweb.models.package_request import ACCEPTED_ID, PENDING_ID, REJECTED_ID @@ -25,14 +22,18 @@ from aurweb.testing.requests import Request @pytest.fixture(autouse=True) def setup(db_test) -> None: - """ Setup the database. """ + """Setup the database.""" return @pytest.fixture def client() -> TestClient: - """ Yield a TestClient. """ - yield TestClient(app=asgi.app) + """Yield a TestClient.""" + client = TestClient(app=asgi.app) + + # disable redirects for our tests + client.follow_redirects = False + yield client def create_user(username: str, email: str) -> User: @@ -44,21 +45,26 @@ def create_user(username: str, email: str) -> User: :return: User instance """ with db.begin(): - user = db.create(User, Username=username, Email=email, - Passwd="testPassword", AccountTypeID=USER_ID) + user = db.create( + User, + Username=username, + Email=email, + Passwd="testPassword", + AccountTypeID=USER_ID, + ) return user @pytest.fixture def user() -> User: - """ Yield a User instance. """ + """Yield a User instance.""" user = create_user("test", "test@example.org") yield user @pytest.fixture def auser(user: User) -> User: - """ Yield an authenticated User instance. """ + """Yield an authenticated User instance.""" cookies = {"AURSID": user.login(Request(), "testPassword")} user.cookies = cookies yield user @@ -66,14 +72,14 @@ def auser(user: User) -> User: @pytest.fixture def user2() -> User: - """ Yield a secondary non-maintainer User instance. """ + """Yield a secondary non-maintainer User instance.""" user = create_user("test2", "test2@example.org") yield user @pytest.fixture def auser2(user2: User) -> User: - """ Yield an authenticated secondary non-maintainer User instance. """ + """Yield an authenticated secondary non-maintainer User instance.""" cookies = {"AURSID": user2.login(Request(), "testPassword")} user2.cookies = cookies yield user2 @@ -81,58 +87,83 @@ def auser2(user2: User) -> User: @pytest.fixture def maintainer() -> User: - """ Yield a specific User used to maintain packages. """ + """Yield a specific User used to maintain packages.""" with db.begin(): - maintainer = db.create(User, Username="test_maintainer", - Email="test_maintainer@example.org", - Passwd="testPassword", - AccountTypeID=USER_ID) + maintainer = db.create( + User, + Username="test_maintainer", + Email="test_maintainer@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield maintainer @pytest.fixture -def packages(maintainer: User) -> List[Package]: - """ Yield 55 packages named pkg_0 .. pkg_54. """ +def maintainer2() -> User: + """Yield a specific User used to maintain packages.""" + with db.begin(): + maintainer = db.create( + User, + Username="test_maintainer2", + Email="test_maintainer2@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) + yield maintainer + + +@pytest.fixture +def packages(maintainer: User, maintainer2: User) -> list[Package]: + """Yield 55 packages named pkg_0 .. pkg_54.""" packages_ = [] now = time.utcnow() with db.begin(): for i in range(55): - pkgbase = db.create(PackageBase, - Name=f"pkg_{i}", - Maintainer=maintainer, - Packager=maintainer, - Submitter=maintainer, - ModifiedTS=now) - package = db.create(Package, - PackageBase=pkgbase, - Name=f"pkg_{i}") + pkgbase = db.create( + PackageBase, + Name=f"pkg_{i}", + Maintainer=maintainer2 if i > 52 else maintainer, + Packager=maintainer, + Submitter=maintainer, + ModifiedTS=now, + ) + package = db.create(Package, PackageBase=pkgbase, Name=f"pkg_{i}") packages_.append(package) yield packages_ @pytest.fixture -def requests(user: User, packages: List[Package]) -> List[PackageRequest]: +def requests( + user: User, maintainer2: User, packages: list[Package] +) -> list[PackageRequest]: pkgreqs = [] with db.begin(): for i in range(55): - pkgreq = db.create(PackageRequest, - ReqTypeID=DELETION_ID, - User=user, - PackageBase=packages[i].PackageBase, - PackageBaseName=packages[i].Name, - Comments=f"Deletion request for pkg_{i}", - ClosureComment=str()) + pkgreq = db.create( + PackageRequest, + ReqTypeID=DELETION_ID, + User=( + maintainer2 + if packages[i].PackageBase.Maintainer.Username == "test_maintainer2" + else user + ), + PackageBase=packages[i].PackageBase, + PackageBaseName=packages[i].Name, + Comments=f"Deletion request for pkg_{i}", + ClosureComment=str(), + ) pkgreqs.append(pkgreq) yield pkgreqs @pytest.fixture -def tu_user() -> User: - """ Yield an authenticated Trusted User instance. """ - user = create_user("test_tu", "test_tu@example.org") +def pm_user() -> User: + """Yield an authenticated Package Maintainer instance.""" + user = create_user("test_pm", "test_pm@example.org") with db.begin(): - user.AccountTypeID = TRUSTED_USER_ID + user.AccountTypeID = PACKAGE_MAINTAINER_ID cookies = {"AURSID": user.login(Request(), "testPassword")} user.cookies = cookies yield user @@ -150,31 +181,38 @@ def create_pkgbase(user: User, name: str) -> PackageBase: """ now = time.utcnow() with db.begin(): - pkgbase = db.create(PackageBase, Name=name, - Maintainer=user, Packager=user, - SubmittedTS=now, ModifiedTS=now) + pkgbase = db.create( + PackageBase, + Name=name, + Maintainer=user, + Packager=user, + SubmittedTS=now, + ModifiedTS=now, + ) db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase) return pkgbase @pytest.fixture def pkgbase(user: User) -> PackageBase: - """ Yield a package base. """ + """Yield a package base.""" pkgbase = create_pkgbase(user, "test-package") yield pkgbase @pytest.fixture def target(user: User) -> PackageBase: - """ Yield a merge target (package base). """ + """Yield a merge target (package base).""" with db.begin(): - target = db.create(PackageBase, Name="target-package", - Maintainer=user, Packager=user) + target = db.create( + PackageBase, Name="target-package", Maintainer=user, Packager=user + ) yield target -def create_request(reqtype_id: int, user: User, pkgbase: PackageBase, - comments: str) -> PackageRequest: +def create_request( + reqtype_id: int, user: User, pkgbase: PackageBase, comments: str +) -> PackageRequest: """ Create a package request based on `reqtype_id`, `user`, `pkgbase` and `comments`. @@ -187,44 +225,49 @@ def create_request(reqtype_id: int, user: User, pkgbase: PackageBase, """ now = time.utcnow() with db.begin(): - pkgreq = db.create(PackageRequest, ReqTypeID=reqtype_id, - User=user, PackageBase=pkgbase, - PackageBaseName=pkgbase.Name, - RequestTS=now, - Comments=comments, - ClosureComment=str()) + pkgreq = db.create( + PackageRequest, + ReqTypeID=reqtype_id, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + RequestTS=now, + Comments=comments, + ClosureComment=str(), + ) return pkgreq @pytest.fixture def pkgreq(user: User, pkgbase: PackageBase): - """ Yield a package request. """ + """Yield a package request.""" pkgreq = create_request(DELETION_ID, user, pkgbase, "Test request.") yield pkgreq def create_notification(user: User, pkgbase: PackageBase): - """ Create a notification for a `user` on `pkgbase`. """ + """Create a notification for a `user` on `pkgbase`.""" with db.begin(): notif = db.create(PackageNotification, User=user, PackageBase=pkgbase) return notif def test_request(client: TestClient, auser: User, pkgbase: PackageBase): - """ Test the standard pkgbase request route GET method. """ + """Test the standard pkgbase request route GET method.""" endpoint = f"/pkgbase/{pkgbase.Name}/request" with client as request: - resp = request.get(endpoint, cookies=auser.cookies) + request.cookies = auser.cookies + resp = request.get(endpoint) assert resp.status_code == int(HTTPStatus.OK) -def test_request_post_deletion(client: TestClient, auser2: User, - pkgbase: PackageBase): - """ Test the POST route for creating a deletion request works. """ +def test_request_post_deletion(client: TestClient, auser2: User, pkgbase: PackageBase): + """Test the POST route for creating a deletion request works.""" endpoint = f"/pkgbase/{pkgbase.Name}/request" data = {"comments": "Test request.", "type": "deletion"} with client as request: - resp = request.post(endpoint, data=data, cookies=auser2.cookies) + request.cookies = auser2.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) pkgreq = pkgbase.requests.first() @@ -239,13 +282,15 @@ def test_request_post_deletion(client: TestClient, auser2: User, assert re.match(expr, email.headers.get("Subject")) -def test_request_post_deletion_as_maintainer(client: TestClient, auser: User, - pkgbase: PackageBase): - """ Test the POST route for creating a deletion request as maint works. """ +def test_request_post_deletion_as_maintainer( + client: TestClient, auser: User, pkgbase: PackageBase +): + """Test the POST route for creating a deletion request as maint works.""" endpoint = f"/pkgbase/{pkgbase.Name}/request" data = {"comments": "Test request.", "type": "deletion"} with client as request: - resp = request.post(endpoint, data=data, cookies=auser.cookies) + request.cookies = auser.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) # Check the pkgreq record got created and accepted. @@ -268,10 +313,13 @@ def test_request_post_deletion_as_maintainer(client: TestClient, auser: User, assert re.match(expr, email.headers.get("Subject")) -def test_request_post_deletion_autoaccept(client: TestClient, auser: User, - pkgbase: PackageBase, - caplog: pytest.LogCaptureFixture): - """ Test the request route for deletion as maintainer. """ +def test_request_post_deletion_autoaccept( + client: TestClient, + auser: User, + pkgbase: PackageBase, + caplog: pytest.LogCaptureFixture, +): + """Test the request route for deletion as maintainer.""" caplog.set_level(DEBUG) now = time.utcnow() @@ -282,12 +330,15 @@ def test_request_post_deletion_autoaccept(client: TestClient, auser: User, endpoint = f"/pkgbase/{pkgbase.Name}/request" data = {"comments": "Test request.", "type": "deletion"} with client as request: - resp = request.post(endpoint, data=data, cookies=auser.cookies) + request.cookies = auser.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) - pkgreq = db.query(PackageRequest).filter( - PackageRequest.PackageBaseName == pkgbase.Name - ).first() + pkgreq = ( + db.query(PackageRequest) + .filter(PackageRequest.PackageBaseName == pkgbase.Name) + .first() + ) assert pkgreq is not None assert pkgreq.ReqTypeID == DELETION_ID assert pkgreq.Status == ACCEPTED_ID @@ -311,9 +362,10 @@ def test_request_post_deletion_autoaccept(client: TestClient, auser: User, assert re.search(expr, caplog.text) -def test_request_post_merge(client: TestClient, auser: User, - pkgbase: PackageBase, target: PackageBase): - """ Test the request route for merge as maintainer. """ +def test_request_post_merge( + client: TestClient, auser: User, pkgbase: PackageBase, target: PackageBase +): + """Test the request route for merge as maintainer.""" endpoint = f"/pkgbase/{pkgbase.Name}/request" data = { "type": "merge", @@ -321,7 +373,8 @@ def test_request_post_merge(client: TestClient, auser: User, "comments": "Test request.", } with client as request: - resp = request.post(endpoint, data=data, cookies=auser.cookies) + request.cookies = auser.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) pkgreq = pkgbase.requests.first() @@ -337,16 +390,16 @@ def test_request_post_merge(client: TestClient, auser: User, assert re.match(expr, email.headers.get("Subject")) -def test_request_post_orphan(client: TestClient, auser: User, - pkgbase: PackageBase): - """ Test the POST route for creating an orphan request works. """ +def test_request_post_orphan(client: TestClient, auser: User, pkgbase: PackageBase): + """Test the POST route for creating an orphan request works.""" endpoint = f"/pkgbase/{pkgbase.Name}/request" data = { "type": "orphan", "comments": "Test request.", } with client as request: - resp = request.post(endpoint, data=data, cookies=auser.cookies) + request.cookies = auser.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) pkgreq = pkgbase.requests.first() @@ -362,9 +415,14 @@ def test_request_post_orphan(client: TestClient, auser: User, assert re.match(expr, email.headers.get("Subject")) -def test_deletion_request(client: TestClient, user: User, tu_user: User, - pkgbase: PackageBase, pkgreq: PackageRequest): - """ Test deleting a package with a preexisting request. """ +def test_deletion_request( + client: TestClient, + user: User, + pm_user: User, + pkgbase: PackageBase, + pkgreq: PackageRequest, +): + """Test deleting a package with a preexisting request.""" # `pkgreq`.ReqTypeID is already DELETION_ID. create_request(DELETION_ID, user, pkgbase, "Other request.") @@ -377,7 +435,8 @@ def test_deletion_request(client: TestClient, user: User, tu_user: User, comments = "Test closure." data = {"comments": comments, "confirm": True} with client as request: - resp = request.post(endpoint, data=data, cookies=tu_user.cookies) + request.cookies = pm_user.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == "/packages" @@ -399,18 +458,18 @@ def test_deletion_request(client: TestClient, user: User, tu_user: User, email = Email(3).parse() subject = r"^AUR Package deleted: [^ ]+$" assert re.match(subject, email.headers.get("Subject")) - body = r"%s [1] deleted %s [2]." % (tu_user.Username, pkgbase.Name) + body = r"%s [1] deleted %s [2]." % (pm_user.Username, pkgbase.Name) assert body in email.body -def test_deletion_autorequest(client: TestClient, tu_user: User, - pkgbase: PackageBase): - """ Test deleting a package without a request. """ +def test_deletion_autorequest(client: TestClient, pm_user: User, pkgbase: PackageBase): + """Test deleting a package without a request.""" # `pkgreq`.ReqTypeID is already DELETION_ID. endpoint = f"/pkgbase/{pkgbase.Name}/delete" data = {"confirm": True} with client as request: - resp = request.post(endpoint, data=data, cookies=tu_user.cookies) + request.cookies = pm_user.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == "/packages" @@ -422,10 +481,36 @@ def test_deletion_autorequest(client: TestClient, tu_user: User, assert "[Autogenerated]" in email.body -def test_merge_request(client: TestClient, user: User, tu_user: User, - pkgbase: PackageBase, target: PackageBase, - pkgreq: PackageRequest): - """ Test merging a package with a pre - existing request. """ +def test_deletion_autorequest_with_comment( + client: TestClient, pm_user: User, pkgbase: PackageBase +): + """Test deleting a package without a request and a comment.""" + # `pkgreq`.ReqTypeID is already DELETION_ID. + endpoint = f"/pkgbase/{pkgbase.Name}/delete" + data = {"confirm": True, "comments": "deleted with comment"} + with client as request: + request.cookies = pm_user.cookies + resp = request.post(endpoint, data=data) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + + assert resp.headers.get("location") == "/packages" + assert Email.count() == 1 + + email = Email(1).parse() + subject = r"^\[PRQ#\d+\] Deletion Request for [^ ]+ Accepted$" + assert re.match(subject, email.headers.get("Subject")) + assert "deleted with comment" in email.body + + +def test_merge_request( + client: TestClient, + user: User, + pm_user: User, + pkgbase: PackageBase, + target: PackageBase, + pkgreq: PackageRequest, +): + """Test merging a package with a pre - existing request.""" with db.begin(): pkgreq.ReqTypeID = MERGE_ID pkgreq.MergeBaseName = target.Name @@ -443,7 +528,8 @@ def test_merge_request(client: TestClient, user: User, tu_user: User, comments = "Test merge closure." data = {"into": target.Name, "comments": comments, "confirm": True} with client as request: - resp = request.post(endpoint, data=data, cookies=tu_user.cookies) + request.cookies = pm_user.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{target.Name}" @@ -474,9 +560,14 @@ def test_merge_request(client: TestClient, user: User, tu_user: User, assert "[Autogenerated]" in rejected.body -def test_merge_autorequest(client: TestClient, user: User, tu_user: User, - pkgbase: PackageBase, target: PackageBase): - """ Test merging a package without a request. """ +def test_merge_autorequest( + client: TestClient, + user: User, + pm_user: User, + pkgbase: PackageBase, + target: PackageBase, +): + """Test merging a package without a request.""" with db.begin(): pkgreq.ReqTypeID = MERGE_ID pkgreq.MergeBaseName = target.Name @@ -485,7 +576,8 @@ def test_merge_autorequest(client: TestClient, user: User, tu_user: User, endpoint = f"/pkgbase/{pkgbase.Name}/merge" data = {"into": target.Name, "confirm": True} with client as request: - resp = request.post(endpoint, data=data, cookies=tu_user.cookies) + request.cookies = pm_user.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{target.Name}" @@ -499,13 +591,48 @@ def test_merge_autorequest(client: TestClient, user: User, tu_user: User, assert "[Autogenerated]" in email.body -def test_orphan_request(client: TestClient, user: User, tu_user: User, - pkgbase: PackageBase, pkgreq: PackageRequest): - """ Test the standard orphan request route. """ +def test_merge_autorequest_with_comment( + client: TestClient, + user: User, + pm_user: User, + pkgbase: PackageBase, + target: PackageBase, +): + """Test merging a package without a request.""" + with db.begin(): + pkgreq.ReqTypeID = MERGE_ID + pkgreq.MergeBaseName = target.Name + + # `pkgreq`.ReqTypeID is already DELETION_ID. + endpoint = f"/pkgbase/{pkgbase.Name}/merge" + data = {"into": target.Name, "confirm": True, "comments": "merged with comment"} + with client as request: + request.cookies = pm_user.cookies + resp = request.post(endpoint, data=data) + assert resp.status_code == int(HTTPStatus.SEE_OTHER) + assert resp.headers.get("location") == f"/pkgbase/{target.Name}" + + # Should've gotten one email with our comment + assert Email.count() == 1 + + # Test accepted merge request notification. + email = Email(1).parse() + subj = r"^\[PRQ#\d+\] Merge Request for [^ ]+ Accepted$" + assert re.match(subj, email.headers.get("Subject")) + assert "merged with comment" in email.body + + +def test_orphan_request( + client: TestClient, + user: User, + pm_user: User, + pkgbase: PackageBase, + pkgreq: PackageRequest, +): + """Test the standard orphan request route.""" user2 = create_user("user2", "user2@example.org") with db.begin(): - db.create(PackageComaintainer, User=user2, - PackageBase=pkgbase, Priority=1) + db.create(PackageComaintainer, User=user2, PackageBase=pkgbase, Priority=1) idle_time = config.getint("options", "request_idle_time") now = time.utcnow() @@ -518,7 +645,8 @@ def test_orphan_request(client: TestClient, user: User, tu_user: User, comments = "Test orphan closure." data = {"comments": comments, "confirm": True} with client as request: - resp = request.post(endpoint, data=data, cookies=tu_user.cookies) + request.cookies = pm_user.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" @@ -538,10 +666,9 @@ def test_orphan_request(client: TestClient, user: User, tu_user: User, assert re.match(subj, email.headers.get("Subject")) -def test_request_post_orphan_autogenerated_closure(client: TestClient, - tu_user: User, - pkgbase: PackageBase, - pkgreq: PackageRequest): +def test_request_post_orphan_autogenerated_closure( + client: TestClient, pm_user: User, pkgbase: PackageBase, pkgreq: PackageRequest +): idle_time = config.getint("options", "request_idle_time") now = time.utcnow() with db.begin(): @@ -552,7 +679,8 @@ def test_request_post_orphan_autogenerated_closure(client: TestClient, endpoint = f"/pkgbase/{pkgbase.Name}/disown" data = {"confirm": True} with client as request: - resp = request.post(endpoint, data=data, cookies=tu_user.cookies) + request.cookies = pm_user.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" @@ -565,10 +693,13 @@ def test_request_post_orphan_autogenerated_closure(client: TestClient, assert re.search(expr, email.body) -def test_request_post_orphan_autoaccept(client: TestClient, auser: User, - pkgbase: PackageBase, - caplog: pytest.LogCaptureFixture): - """ Test the standard pkgbase request route GET method. """ +def test_request_post_orphan_autoaccept( + client: TestClient, + auser: User, + pkgbase: PackageBase, + caplog: pytest.LogCaptureFixture, +): + """Test the standard pkgbase request route GET method.""" caplog.set_level(DEBUG) now = time.utcnow() auto_orphan_age = config.getint("options", "auto_orphan_age") @@ -581,7 +712,8 @@ def test_request_post_orphan_autoaccept(client: TestClient, auser: User, "comments": "Test request.", } with client as request: - resp = request.post(endpoint, data=data, cookies=auser.cookies) + request.cookies = auser.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) pkgreq = pkgbase.requests.first() @@ -606,12 +738,12 @@ def test_request_post_orphan_autoaccept(client: TestClient, auser: User, assert re.search(expr, caplog.text) -def test_orphan_as_maintainer(client: TestClient, auser: User, - pkgbase: PackageBase): +def test_orphan_as_maintainer(client: TestClient, auser: User, pkgbase: PackageBase): endpoint = f"/pkgbase/{pkgbase.Name}/disown" data = {"confirm": True} with client as request: - resp = request.post(endpoint, data=data, cookies=auser.cookies) + request.cookies = auser.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == f"/pkgbase/{pkgbase.Name}" @@ -621,13 +753,15 @@ def test_orphan_as_maintainer(client: TestClient, auser: User, assert pkgbase.Maintainer is None -def test_orphan_without_requests(client: TestClient, tu_user: User, - pkgbase: PackageBase): - """ Test orphans are automatically accepted past a certain date. """ +def test_orphan_without_requests( + client: TestClient, pm_user: User, pkgbase: PackageBase +): + """Test orphans are automatically accepted past a certain date.""" endpoint = f"/pkgbase/{pkgbase.Name}/disown" data = {"confirm": True} with client as request: - resp = request.post(endpoint, data=data, cookies=tu_user.cookies) + request.cookies = pm_user.cookies + resp = request.post(endpoint, data=data) assert resp.status_code == int(HTTPStatus.BAD_REQUEST) errors = get_errors(resp.text) @@ -638,7 +772,7 @@ def test_orphan_without_requests(client: TestClient, tu_user: User, def test_closure_factory_invalid_reqtype_id(): - """ Test providing an invalid reqtype_id raises NotImplementedError. """ + """Test providing an invalid reqtype_id raises NotImplementedError.""" automated = ClosureFactory() match = r"^Unsupported '.+' value\.$" with pytest.raises(NotImplementedError, match=match): @@ -654,23 +788,29 @@ def test_pkgreq_by_id_not_found(): def test_requests_unauthorized(client: TestClient): with client as request: - resp = request.get("/requests", allow_redirects=False) + resp = request.get("/requests") assert resp.status_code == int(HTTPStatus.SEE_OTHER) -def test_requests(client: TestClient, - tu_user: User, - packages: List[Package], - requests: List[PackageRequest]): - cookies = {"AURSID": tu_user.login(Request(), "testPassword")} +def test_requests( + client: TestClient, + pm_user: User, + packages: list[Package], + requests: list[PackageRequest], +): + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} with client as request: - resp = request.get("/requests", params={ - # Pass in url query parameters O, SeB and SB to exercise - # their paths inside of the pager_nav used in this request. - "O": 0, # Page 1 - "SeB": "nd", - "SB": "n" - }, cookies=cookies) + request.cookies = cookies + resp = request.get( + "/requests", + params={ + # Pass in url query parameters O, SeB and SB to exercise + # their paths inside of the pager_nav used in this request. + "O": 0, # Page 1 + "SeB": "nd", + "SB": "n", + }, + ) assert resp.status_code == int(HTTPStatus.OK) assert "Next ›" in resp.text @@ -683,9 +823,76 @@ def test_requests(client: TestClient, # Request page 2 of the requests page. with client as request: - resp = request.get("/requests", params={ - "O": 50 # Page 2 - }, cookies=cookies) + request.cookies = cookies + resp = request.get("/requests", params={"O": 50}) # Page 2 + assert resp.status_code == int(HTTPStatus.OK) + + assert "‹ Previous" in resp.text + assert "« First" in resp.text + + root = parse_root(resp.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 5 # There are five records left on the second page. + + # Delete requesters user account and check output + with db.begin(): + db.delete(requests[0].User) + + with client as request: + request.cookies = cookies + resp = request.get("/requests") + + assert "(deleted)" in resp.text + + +def test_requests_with_filters( + client: TestClient, + pm_user: User, + packages: list[Package], + requests: list[PackageRequest], +): + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} + with client as request: + request.cookies = cookies + resp = request.get( + "/requests", + params={ + # Pass in url query parameters O, SeB and SB to exercise + # their paths inside of the pager_nav used in this request. + "O": 0, # Page 1 + "SeB": "nd", + "SB": "n", + "filter_pending": True, + "filter_closed": True, + "filter_accepted": True, + "filter_rejected": True, + "filter_maintainer_requests": False, + }, + ) + assert resp.status_code == int(HTTPStatus.OK) + + assert "Next ›" in resp.text + assert "Last »" in resp.text + + root = parse_root(resp.text) + # We have 55 requests, our defaults.PP is 50, so expect we have 50 rows. + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == defaults.PP + + # Request page 2 of the requests page. + with client as request: + request.cookies = cookies + resp = request.get( + "/requests", + params={ + "O": 50, + "filter_pending": True, + "filter_closed": True, + "filter_accepted": True, + "filter_rejected": True, + "filter_maintainer_requests": False, + }, + ) # Page 2 assert resp.status_code == int(HTTPStatus.OK) assert "‹ Previous" in resp.text @@ -696,11 +903,103 @@ def test_requests(client: TestClient, assert len(rows) == 5 # There are five records left on the second page. -def test_requests_selfmade(client: TestClient, user: User, - requests: List[PackageRequest]): +def test_requests_for_maintainer_requests( + client: TestClient, + pm_user: User, + packages: list[Package], + requests: list[PackageRequest], +): + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} + with client as request: + request.cookies = cookies + resp = request.get( + "/requests", + params={"filter_maintainer_requests": True}, + ) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + # We only expect 2 requests since we are looking for requests from the maintainers + assert len(rows) == 2 + + +def test_requests_with_package_name_filter( + client: TestClient, + pm_user: User, + user2: User, + packages: list[Package], + requests: list[PackageRequest], +): + # test as PM + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} + with client as request: + request.cookies = cookies + resp = request.get( + "/requests", + params={"filter_pkg_name": "kg_1"}, + ) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + # We expect 11 requests for all packages containing "kg_1" + assert len(rows) == 11 + + # test as PM, no results + with client as request: + request.cookies = cookies + resp = request.get( + "/requests", + params={"filter_pkg_name": "x"}, + ) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + # We expect 0 requests since we don't have anything containing "x" + assert len(rows) == 0 + + # test as regular user, not related to our package + cookies = {"AURSID": user2.login(Request(), "testPassword")} + with client as request: + request.cookies = cookies + resp = request.get( + "/requests", + params={"filter_pkg_name": packages[0].PackageBase.Name}, + ) + assert resp.status_code == int(HTTPStatus.OK) + + root = parse_root(resp.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + # We don't expect to get any requests + assert len(rows) == 0 + + +def test_requests_by_deleted_users( + client: TestClient, user: User, pm_user: User, pkgreq: PackageRequest +): + with db.begin(): + db.delete(user) + + cookies = {"AURSID": pm_user.login(Request(), "testPassword")} + with client as request: + request.cookies = cookies + resp = request.get("/requests") + assert resp.status_code == HTTPStatus.OK + + root = parse_root(resp.text) + rows = root.xpath('//table[@class="results"]/tbody/tr') + assert len(rows) == 1 + + +def test_requests_selfmade( + client: TestClient, user: User, requests: list[PackageRequest] +): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get("/requests", cookies=cookies) + request.cookies = cookies + resp = request.get("/requests") assert resp.status_code == int(HTTPStatus.OK) # As the user who creates all of the requests, we should see all of them. @@ -711,46 +1010,50 @@ def test_requests_selfmade(client: TestClient, user: User, # Our first and only link in the last row should be "Close". for row in rows: - last_row = row.xpath('./td')[-1].xpath('./a')[0] + last_row = row.xpath("./td")[-1].xpath("./a")[0] assert last_row.text.strip() == "Close" -def test_requests_close(client: TestClient, user: User, - pkgreq: PackageRequest): +def test_requests_close(client: TestClient, user: User, pkgreq: PackageRequest): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.get(f"/requests/{pkgreq.ID}/close", cookies=cookies, - allow_redirects=False) + request.cookies = cookies + resp = request.get(f"/requests/{pkgreq.ID}/close") assert resp.status_code == int(HTTPStatus.OK) -def test_requests_close_unauthorized(client: TestClient, maintainer: User, - pkgreq: PackageRequest): +def test_requests_close_unauthorized( + client: TestClient, maintainer: User, pkgreq: PackageRequest +): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - resp = request.get(f"/requests/{pkgreq.ID}/close", cookies=cookies, - allow_redirects=False) + request.cookies = cookies + resp = request.get( + f"/requests/{pkgreq.ID}/close", + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == "/" -def test_requests_close_post_unauthorized(client: TestClient, maintainer: User, - pkgreq: PackageRequest): +def test_requests_close_post_unauthorized( + client: TestClient, maintainer: User, pkgreq: PackageRequest +): cookies = {"AURSID": maintainer.login(Request(), "testPassword")} with client as request: - resp = request.post(f"/requests/{pkgreq.ID}/close", data={ - "reason": ACCEPTED_ID - }, cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.post( + f"/requests/{pkgreq.ID}/close", + data={"reason": ACCEPTED_ID}, + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert resp.headers.get("location") == "/" -def test_requests_close_post(client: TestClient, user: User, - pkgreq: PackageRequest): +def test_requests_close_post(client: TestClient, user: User, pkgreq: PackageRequest): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(f"/requests/{pkgreq.ID}/close", - cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.post(f"/requests/{pkgreq.ID}/close") assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert pkgreq.Status == REJECTED_ID @@ -758,12 +1061,15 @@ def test_requests_close_post(client: TestClient, user: User, assert pkgreq.ClosureComment == str() -def test_requests_close_post_rejected(client: TestClient, user: User, - pkgreq: PackageRequest): +def test_requests_close_post_rejected( + client: TestClient, user: User, pkgreq: PackageRequest +): cookies = {"AURSID": user.login(Request(), "testPassword")} with client as request: - resp = request.post(f"/requests/{pkgreq.ID}/close", - cookies=cookies, allow_redirects=False) + request.cookies = cookies + resp = request.post( + f"/requests/{pkgreq.ID}/close", + ) assert resp.status_code == int(HTTPStatus.SEE_OTHER) assert pkgreq.Status == REJECTED_ID diff --git a/test/test_routes.py b/test/test_routes.py index 85d30c02..c104211e 100644 --- a/test/test_routes.py +++ b/test/test_routes.py @@ -1,11 +1,9 @@ import re import urllib.parse - from http import HTTPStatus import lxml.etree import pytest - from fastapi.testclient import TestClient from aurweb import db @@ -22,27 +20,36 @@ def setup(db_test): @pytest.fixture def client() -> TestClient: - yield TestClient(app=app) + client = TestClient(app=app) + + # disable redirects for our tests + client.follow_redirects = False + yield client @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user def test_index(client: TestClient): - """ Test the index route at '/'. """ + """Test the index route at '/'.""" with client as req: response = req.get("/") assert response.status_code == int(HTTPStatus.OK) def test_index_security_headers(client: TestClient): - """ Check for the existence of CSP, XCTO, XFO and RP security headers. + """Check for the existence of CSP, XCTO, XFO and RP security headers. CSP: Content-Security-Policy XCTO: X-Content-Type-Options @@ -60,61 +67,48 @@ def test_index_security_headers(client: TestClient): def test_favicon(client: TestClient): - """ Test the favicon route at '/favicon.ico'. """ + """Test the favicon route at '/favicon.ico'.""" with client as request: response1 = request.get("/static/images/favicon.ico") - response2 = request.get("/favicon.ico") + response2 = request.get("/favicon.ico", follow_redirects=True) assert response1.status_code == int(HTTPStatus.OK) assert response1.content == response2.content def test_language(client: TestClient): - """ Test the language post route as a guest user. """ - post_data = { - "set_lang": "de", - "next": "/" - } + """Test the language post route as a guest user.""" + post_data = {"set_lang": "de", "next": "/"} with client as req: response = req.post("/language", data=post_data) assert response.status_code == int(HTTPStatus.SEE_OTHER) def test_language_invalid_next(client: TestClient): - """ Test an invalid next route at '/language'. """ - post_data = { - "set_lang": "de", - "next": "https://evil.net" - } + """Test an invalid next route at '/language'.""" + post_data = {"set_lang": "de", "next": "https://evil.net"} with client as req: response = req.post("/language", data=post_data) assert response.status_code == int(HTTPStatus.BAD_REQUEST) def test_user_language(client: TestClient, user: User): - """ Test the language post route as an authenticated user. """ - post_data = { - "set_lang": "de", - "next": "/" - } + """Test the language post route as an authenticated user.""" + post_data = {"set_lang": "de", "next": "/"} sid = user.login(Request(), "testPassword") assert sid is not None with client as req: - response = req.post("/language", data=post_data, - cookies={"AURSID": sid}) + req.cookies = {"AURSID": sid} + response = req.post("/language", data=post_data) assert response.status_code == int(HTTPStatus.SEE_OTHER) assert user.LangPreference == "de" def test_language_query_params(client: TestClient): - """ Test the language post route with query params. """ + """Test the language post route with query params.""" next = urllib.parse.quote_plus("/") - post_data = { - "set_lang": "de", - "next": "/", - "q": f"next={next}" - } + post_data = {"set_lang": "de", "next": "/", "q": f"next={next}"} q = post_data.get("q") with client as req: response = req.post("/language", data=post_data) @@ -154,9 +148,12 @@ def test_nonce_csp(client: TestClient): def test_id_redirect(client: TestClient): with client as request: - response = request.get("/", params={ - "id": "test", # This param will be rewritten into Location. - "key": "value", # Test that this param persists. - "key2": "value2" # And this one. - }, allow_redirects=False) + response = request.get( + "/", + params={ + "id": "test", # This param will be rewritten into Location. + "key": "value", # Test that this param persists. + "key2": "value2", # And this one. + }, + ) assert response.headers.get("location") == "/test?key=value&key2=value2" diff --git a/test/test_rpc.py b/test/test_rpc.py index 2f7f7860..a2256700 100644 --- a/test/test_rpc.py +++ b/test/test_rpc.py @@ -1,32 +1,31 @@ import re - from http import HTTPStatus -from typing import List from unittest import mock import orjson import pytest - from fastapi.testclient import TestClient from redis.client import Pipeline import aurweb.models.dependency_type as dt import aurweb.models.relation_type as rt - from aurweb import asgi, config, db, rpc, scripts, time +from aurweb.aur_redis import redis_connection from aurweb.models.account_type import USER_ID from aurweb.models.dependency_type import DEPENDS_ID +from aurweb.models.group import Group from aurweb.models.license import License from aurweb.models.package import Package from aurweb.models.package_base import PackageBase +from aurweb.models.package_comaintainer import PackageComaintainer from aurweb.models.package_dependency import PackageDependency +from aurweb.models.package_group import PackageGroup from aurweb.models.package_keyword import PackageKeyword from aurweb.models.package_license import PackageLicense from aurweb.models.package_relation import PackageRelation from aurweb.models.package_vote import PackageVote from aurweb.models.relation_type import PROVIDES_ID from aurweb.models.user import User -from aurweb.redis import redis_connection @pytest.fixture @@ -37,165 +36,269 @@ def client() -> TestClient: @pytest.fixture def user(db_test) -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User 1", Passwd=str(), - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User 1", + Passwd=str(), + AccountTypeID=USER_ID, + ) yield user @pytest.fixture def user2() -> User: with db.begin(): - user = db.create(User, Username="user2", Email="user2@example.org", - RealName="Test User 2", Passwd=str(), - AccountTypeID=USER_ID) + user = db.create( + User, + Username="user2", + Email="user2@example.org", + RealName="Test User 2", + Passwd=str(), + AccountTypeID=USER_ID, + ) yield user @pytest.fixture def user3() -> User: with db.begin(): - user = db.create(User, Username="user3", Email="user3@example.org", - RealName="Test User 3", Passwd=str(), - AccountTypeID=USER_ID) + user = db.create( + User, + Username="user3", + Email="user3@example.org", + RealName="Test User 3", + Passwd=str(), + AccountTypeID=USER_ID, + ) yield user @pytest.fixture -def packages(user: User, user2: User, user3: User) -> List[Package]: +def packages(user: User, user2: User, user3: User) -> list[Package]: output = [] # Create package records used in our tests. with db.begin(): - pkgbase = db.create(PackageBase, Name="big-chungus", - Maintainer=user, Packager=user) - pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, - Description="Bunny bunny around bunny", - URL="https://example.com/") + pkgbase = db.create( + PackageBase, + Name="big-chungus", + Maintainer=user, + Packager=user, + Submitter=user2, + ) + pkg = db.create( + Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="Bunny bunny around bunny", + URL="https://example.com/", + ) output.append(pkg) - pkgbase = db.create(PackageBase, Name="chungy-chungus", - Maintainer=user, Packager=user) - pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, - Description="Wubby wubby on wobba wuubu", - URL="https://example.com/") + pkgbase = db.create( + PackageBase, + Name="chungy-chungus", + Maintainer=user, + Packager=user, + Submitter=user2, + ) + pkg = db.create( + Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="Wubby wubby on wobba wuubu", + URL="https://example.com/", + ) output.append(pkg) - pkgbase = db.create(PackageBase, Name="gluggly-chungus", - Maintainer=user, Packager=user) - pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, - Description="glurrba glurrba gur globba", - URL="https://example.com/") + pkgbase = db.create( + PackageBase, Name="gluggly-chungus", Maintainer=user, Packager=user + ) + pkg = db.create( + Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="glurrba glurrba gur globba", + URL="https://example.com/", + ) output.append(pkg) - pkgbase = db.create(PackageBase, Name="fugly-chungus", - Maintainer=user, Packager=user) + pkgbase = db.create( + PackageBase, Name="fugly-chungus", Maintainer=user, Packager=user + ) desc = "A Package belonging to a PackageBase with another name." - pkg = db.create(Package, PackageBase=pkgbase, Name="other-pkg", - Description=desc, URL="https://example.com") + pkg = db.create( + Package, + PackageBase=pkgbase, + Name="other-pkg", + Description=desc, + URL="https://example.com", + ) output.append(pkg) pkgbase = db.create(PackageBase, Name="woogly-chungus") - pkg = db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name, - Description="wuggla woblabeloop shemashmoop", - URL="https://example.com/") + pkg = db.create( + Package, + PackageBase=pkgbase, + Name=pkgbase.Name, + Description="wuggla woblabeloop shemashmoop", + URL="https://example.com/", + ) output.append(pkg) # Setup a few more related records on the first package: - # a license, some keywords and some votes. + # a license, group, some keywords, comaintainer and some votes. with db.begin(): lic = db.create(License, Name="GPL") db.create(PackageLicense, Package=output[0], License=lic) + grp = db.create(Group, Name="testgroup") + db.create(PackageGroup, Package=output[0], Group=grp) + + db.create( + PackageComaintainer, + PackageBase=output[0].PackageBase, + User=user2, + Priority=1, + ) + for keyword in ["big-chungus", "smol-chungus", "sizeable-chungus"]: - db.create(PackageKeyword, - PackageBase=output[0].PackageBase, - Keyword=keyword) + db.create( + PackageKeyword, PackageBase=output[0].PackageBase, Keyword=keyword + ) now = time.utcnow() for user_ in [user, user2, user3]: - db.create(PackageVote, User=user_, - PackageBase=output[0].PackageBase, VoteTS=now) + db.create( + PackageVote, User=user_, PackageBase=output[0].PackageBase, VoteTS=now + ) scripts.popupdate.run_single(output[0].PackageBase) yield output @pytest.fixture -def depends(packages: List[Package]) -> List[PackageDependency]: +def depends(packages: list[Package]) -> list[PackageDependency]: output = [] with db.begin(): - dep = db.create(PackageDependency, - Package=packages[0], - DepTypeID=dt.DEPENDS_ID, - DepName="chungus-depends") + dep = db.create( + PackageDependency, + Package=packages[0], + DepTypeID=dt.DEPENDS_ID, + DepName="chungus-depends", + ) output.append(dep) - dep = db.create(PackageDependency, - Package=packages[1], - DepTypeID=dt.DEPENDS_ID, - DepName="chungy-depends") + dep = db.create( + PackageDependency, + Package=packages[1], + DepTypeID=dt.DEPENDS_ID, + DepName="chungy-depends", + ) output.append(dep) - dep = db.create(PackageDependency, - Package=packages[0], - DepTypeID=dt.OPTDEPENDS_ID, - DepName="chungus-optdepends", - DepCondition="=50") + dep = db.create( + PackageDependency, + Package=packages[0], + DepTypeID=dt.OPTDEPENDS_ID, + DepName="chungus-optdepends", + DepCondition="=50", + ) output.append(dep) - dep = db.create(PackageDependency, - Package=packages[0], - DepTypeID=dt.MAKEDEPENDS_ID, - DepName="chungus-makedepends") + dep = db.create( + PackageDependency, + Package=packages[0], + DepTypeID=dt.MAKEDEPENDS_ID, + DepName="chungus-makedepends", + ) output.append(dep) - dep = db.create(PackageDependency, - Package=packages[0], - DepTypeID=dt.CHECKDEPENDS_ID, - DepName="chungus-checkdepends") + dep = db.create( + PackageDependency, + Package=packages[0], + DepTypeID=dt.CHECKDEPENDS_ID, + DepName="chungus-checkdepends", + ) output.append(dep) yield output @pytest.fixture -def relations(user: User, packages: List[Package]) -> List[PackageRelation]: +def relations(user: User, packages: list[Package]) -> list[PackageRelation]: output = [] with db.begin(): - rel = db.create(PackageRelation, - Package=packages[0], - RelTypeID=rt.CONFLICTS_ID, - RelName="chungus-conflicts") + rel = db.create( + PackageRelation, + Package=packages[0], + RelTypeID=rt.CONFLICTS_ID, + RelName="chungus-conflicts", + ) output.append(rel) - rel = db.create(PackageRelation, - Package=packages[1], - RelTypeID=rt.CONFLICTS_ID, - RelName="chungy-conflicts") + rel = db.create( + PackageRelation, + Package=packages[1], + RelTypeID=rt.CONFLICTS_ID, + RelName="chungy-conflicts", + ) output.append(rel) - rel = db.create(PackageRelation, - Package=packages[0], - RelTypeID=rt.PROVIDES_ID, - RelName="chungus-provides", - RelCondition="<=200") + rel = db.create( + PackageRelation, + Package=packages[0], + RelTypeID=rt.PROVIDES_ID, + RelName="chungus-provides", + RelCondition="<=200", + ) output.append(rel) - rel = db.create(PackageRelation, - Package=packages[0], - RelTypeID=rt.REPLACES_ID, - RelName="chungus-replaces", - RelCondition="<=200") + rel = db.create( + PackageRelation, + Package=packages[0], + RelTypeID=rt.REPLACES_ID, + RelName="chungus-replaces", + RelCondition="<=200", + ) output.append(rel) # Finally, yield the packages. yield output +@pytest.fixture +def comaintainer( + user2: User, user3: User, packages: list[Package] +) -> list[PackageComaintainer]: + output = [] + + with db.begin(): + comaintainer = db.create( + PackageComaintainer, + User=user2, + PackageBase=packages[0].PackageBase, + Priority=1, + ) + output.append(comaintainer) + + comaintainer = db.create( + PackageComaintainer, + User=user3, + PackageBase=packages[0].PackageBase, + Priority=1, + ) + output.append(comaintainer) + + # Finally, yield the packages. + yield output + + @pytest.fixture(autouse=True) def setup(db_test): # Create some extra package relationships. @@ -207,8 +310,8 @@ def pipeline(): redis = redis_connection() pipeline = redis.pipeline() - # The 'testclient' host is used when requesting the app - # via fastapi.testclient.TestClient. + # 'testclient' is our fallback value in case request.client is None + # which is the case for TestClient pipeline.delete("ratelimit-ws:testclient") pipeline.delete("ratelimit:testclient") pipeline.execute() @@ -239,51 +342,59 @@ def test_rpc_documentation_missing(): config.rehash() -def test_rpc_singular_info(client: TestClient, - user: User, - packages: List[Package], - depends: List[PackageDependency], - relations: List[PackageRelation]): +def test_rpc_singular_info( + client: TestClient, + user: User, + user2: User, + packages: list[Package], + depends: list[PackageDependency], + relations: list[PackageRelation], + comaintainer: list[PackageComaintainer], +): # Define expected response. pkg = packages[0] expected_data = { "version": 5, - "results": [{ - "Name": pkg.Name, - "Version": pkg.Version, - "Description": pkg.Description, - "URL": pkg.URL, - "PackageBase": pkg.PackageBase.Name, - "NumVotes": pkg.PackageBase.NumVotes, - "Popularity": float(pkg.PackageBase.Popularity), - "OutOfDate": None, - "Maintainer": user.Username, - "URLPath": f"/cgit/aur.git/snapshot/{pkg.Name}.tar.gz", - "Depends": ["chungus-depends"], - "OptDepends": ["chungus-optdepends=50"], - "MakeDepends": ["chungus-makedepends"], - "CheckDepends": ["chungus-checkdepends"], - "Conflicts": ["chungus-conflicts"], - "Provides": ["chungus-provides<=200"], - "Replaces": ["chungus-replaces<=200"], - "License": [pkg.package_licenses.first().License.Name], - "Keywords": [ - "big-chungus", - "sizeable-chungus", - "smol-chungus" - ] - }], + "results": [ + { + "Name": pkg.Name, + "Version": pkg.Version, + "Description": pkg.Description, + "URL": pkg.URL, + "PackageBase": pkg.PackageBase.Name, + "NumVotes": pkg.PackageBase.NumVotes, + "Popularity": float(pkg.PackageBase.Popularity), + "OutOfDate": None, + "Maintainer": user.Username, + "Submitter": user2.Username, + "URLPath": f"/cgit/aur.git/snapshot/{pkg.Name}.tar.gz", + "Depends": ["chungus-depends"], + "OptDepends": ["chungus-optdepends=50"], + "MakeDepends": ["chungus-makedepends"], + "CheckDepends": ["chungus-checkdepends"], + "Conflicts": ["chungus-conflicts"], + "CoMaintainers": ["user2", "user3"], + "Provides": ["chungus-provides<=200"], + "Replaces": ["chungus-replaces<=200"], + "License": [pkg.package_licenses.first().License.Name], + "Keywords": ["big-chungus", "sizeable-chungus", "smol-chungus"], + "Groups": ["testgroup"], + } + ], "resultcount": 1, - "type": "multiinfo" + "type": "multiinfo", } # Make dummy request. with client as request: - resp = request.get("/rpc", params={ - "v": 5, - "type": "info", - "arg": ["chungy-chungus", "big-chungus"], - }) + resp = request.get( + "/rpc", + params={ + "v": 5, + "type": "info", + "arg": ["chungy-chungus", "big-chungus"], + }, + ) # Load request response into Python dictionary. response_data = orjson.loads(resp.text) @@ -298,6 +409,30 @@ def test_rpc_singular_info(client: TestClient, assert response_data == expected_data +def test_rpc_split_package_urlpath(client: TestClient, user: User): + with db.begin(): + pkgbase = db.create(PackageBase, Name="pkg", Maintainer=user, Packager=user) + pkgs = [ + db.create(Package, PackageBase=pkgbase, Name="pkg_1"), + db.create(Package, PackageBase=pkgbase, Name="pkg_2"), + ] + + with client as request: + response = request.get( + "/rpc", + params={ + "v": 5, + "type": "info", + "arg": [pkgs[0].Name], + }, + ) + + data = orjson.loads(response.text) + snapshot_uri = config.get("options", "snapshot_uri") + urlpath = data.get("results")[0].get("URLPath") + assert urlpath == (snapshot_uri % pkgbase.Name) + + def test_rpc_nonexistent_package(client: TestClient): # Make dummy request. with client as request: @@ -310,13 +445,13 @@ def test_rpc_nonexistent_package(client: TestClient): assert response_data["resultcount"] == 0 -def test_rpc_multiinfo(client: TestClient, packages: List[Package]): +def test_rpc_multiinfo(client: TestClient, packages: list[Package]): # Make dummy request. request_packages = ["big-chungus", "chungy-chungus"] with client as request: - response = request.get("/rpc", params={ - "v": 5, "type": "info", "arg[]": request_packages - }) + response = request.get( + "/rpc", params={"v": 5, "type": "info", "arg[]": request_packages} + ) # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -328,7 +463,7 @@ def test_rpc_multiinfo(client: TestClient, packages: List[Package]): assert request_packages == [] -def test_rpc_mixedargs(client: TestClient, packages: List[Package]): +def test_rpc_mixedargs(client: TestClient, packages: list[Package]): # Make dummy request. response1_packages = ["gluggly-chungus"] response2_packages = ["gluggly-chungus", "chungy-chungus"] @@ -336,13 +471,15 @@ def test_rpc_mixedargs(client: TestClient, packages: List[Package]): with client as request: # Supply all of the args in the url to enforce ordering. response1 = request.get( - "/rpc?v=5&arg[]=big-chungus&arg=gluggly-chungus&type=info") + "/rpc?v=5&arg[]=big-chungus&arg=gluggly-chungus&type=info" + ) assert response1.status_code == int(HTTPStatus.OK) with client as request: response2 = request.get( "/rpc?v=5&arg=big-chungus&arg[]=gluggly-chungus" - "&type=info&arg[]=chungy-chungus") + "&type=info&arg[]=chungy-chungus" + ) assert response1.status_code == int(HTTPStatus.OK) # Load request response into Python dictionary. @@ -360,42 +497,49 @@ def test_rpc_mixedargs(client: TestClient, packages: List[Package]): assert i == [] -def test_rpc_no_dependencies_omits_key(client: TestClient, user: User, - packages: List[Package], - depends: List[PackageDependency], - relations: List[PackageRelation]): +def test_rpc_no_dependencies_omits_key( + client: TestClient, + user: User, + user2: User, + packages: list[Package], + depends: list[PackageDependency], + relations: list[PackageRelation], +): """ This makes sure things like 'MakeDepends' get removed from JSON strings when they don't have set values. """ pkg = packages[1] expected_response = { - 'version': 5, - 'results': [{ - 'Name': pkg.Name, - 'Version': pkg.Version, - 'Description': pkg.Description, - 'URL': pkg.URL, - 'PackageBase': pkg.PackageBase.Name, - 'NumVotes': pkg.PackageBase.NumVotes, - 'Popularity': int(pkg.PackageBase.Popularity), - 'OutOfDate': None, - 'Maintainer': user.Username, - 'URLPath': '/cgit/aur.git/snapshot/chungy-chungus.tar.gz', - 'Depends': ['chungy-depends'], - 'Conflicts': ['chungy-conflicts'], - 'License': [], - 'Keywords': [] - }], - 'resultcount': 1, - 'type': 'multiinfo' + "version": 5, + "results": [ + { + "Name": pkg.Name, + "Version": pkg.Version, + "Description": pkg.Description, + "URL": pkg.URL, + "PackageBase": pkg.PackageBase.Name, + "NumVotes": pkg.PackageBase.NumVotes, + "Popularity": int(pkg.PackageBase.Popularity), + "OutOfDate": None, + "Maintainer": user.Username, + "Submitter": user2.Username, + "URLPath": "/cgit/aur.git/snapshot/chungy-chungus.tar.gz", + "Depends": ["chungy-depends"], + "Conflicts": ["chungy-conflicts"], + "License": [], + "Keywords": [], + } + ], + "resultcount": 1, + "type": "multiinfo", } # Make dummy request. with client as request: - response = request.get("/rpc", params={ - "v": 5, "type": "info", "arg": "chungy-chungus" - }) + response = request.get( + "/rpc", params={"v": 5, "type": "info", "arg": "chungy-chungus"} + ) response_data = orjson.loads(response.content.decode()) # Remove inconsistent keys. @@ -408,18 +552,18 @@ def test_rpc_no_dependencies_omits_key(client: TestClient, user: User, def test_rpc_bad_type(client: TestClient): # Define expected response. expected_data = { - 'version': 5, - 'results': [], - 'resultcount': 0, - 'type': 'error', - 'error': 'Incorrect request type specified.' + "version": 5, + "results": [], + "resultcount": 0, + "type": "error", + "error": "Incorrect request type specified.", } # Make dummy request. with client as request: - response = request.get("/rpc", params={ - "v": 5, "type": "invalid-type", "arg": "big-chungus" - }) + response = request.get( + "/rpc", params={"v": 5, "type": "invalid-type", "arg": "big-chungus"} + ) # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -431,18 +575,18 @@ def test_rpc_bad_type(client: TestClient): def test_rpc_bad_version(client: TestClient): # Define expected response. expected_data = { - 'version': 0, - 'resultcount': 0, - 'results': [], - 'type': 'error', - 'error': 'Invalid version specified.' + "version": 0, + "resultcount": 0, + "results": [], + "type": "error", + "error": "Invalid version specified.", } # Make dummy request. with client as request: - response = request.get("/rpc", params={ - "v": 0, "type": "info", "arg": "big-chungus" - }) + response = request.get( + "/rpc", params={"v": 0, "type": "info", "arg": "big-chungus"} + ) # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -454,19 +598,16 @@ def test_rpc_bad_version(client: TestClient): def test_rpc_no_version(client: TestClient): # Define expected response. expected_data = { - 'version': None, - 'resultcount': 0, - 'results': [], - 'type': 'error', - 'error': 'Please specify an API version.' + "version": None, + "resultcount": 0, + "results": [], + "type": "error", + "error": "Please specify an API version.", } # Make dummy request. with client as request: - response = request.get("/rpc", params={ - "type": "info", - "arg": "big-chungus" - }) + response = request.get("/rpc", params={"type": "info", "arg": "big-chungus"}) # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -478,11 +619,11 @@ def test_rpc_no_version(client: TestClient): def test_rpc_no_type(client: TestClient): # Define expected response. expected_data = { - 'version': 5, - 'results': [], - 'resultcount': 0, - 'type': 'error', - 'error': 'No request type/data specified.' + "version": 5, + "results": [], + "resultcount": 0, + "type": "error", + "error": "No request type/data specified.", } # Make dummy request. @@ -499,11 +640,11 @@ def test_rpc_no_type(client: TestClient): def test_rpc_no_args(client: TestClient): # Define expected response. expected_data = { - 'version': 5, - 'results': [], - 'resultcount': 0, - 'type': 'error', - 'error': 'No request type/data specified.' + "version": 5, + "results": [], + "resultcount": 0, + "type": "error", + "error": "No request type/data specified.", } # Make dummy request. @@ -517,12 +658,12 @@ def test_rpc_no_args(client: TestClient): assert expected_data == response_data -def test_rpc_no_maintainer(client: TestClient, packages: List[Package]): +def test_rpc_no_maintainer(client: TestClient, packages: list[Package]): # Make dummy request. with client as request: - response = request.get("/rpc", params={ - "v": 5, "type": "info", "arg": "woogly-chungus" - }) + response = request.get( + "/rpc", params={"v": 5, "type": "info", "arg": "woogly-chungus"} + ) # Load request response into Python dictionary. response_data = orjson.loads(response.content.decode()) @@ -531,7 +672,7 @@ def test_rpc_no_maintainer(client: TestClient, packages: List[Package]): assert response_data["results"][0]["Maintainer"] is None -def test_rpc_suggest_pkgbase(client: TestClient, packages: List[Package]): +def test_rpc_suggest_pkgbase(client: TestClient, packages: list[Package]): params = {"v": 5, "type": "suggest-pkgbase", "arg": "big"} with client as request: response = request.get("/rpc", params=params) @@ -560,7 +701,7 @@ def test_rpc_suggest_pkgbase(client: TestClient, packages: List[Package]): assert data == [] -def test_rpc_suggest(client: TestClient, packages: List[Package]): +def test_rpc_suggest(client: TestClient, packages: list[Package]): params = {"v": 5, "type": "suggest", "arg": "other"} with client as request: response = request.get("/rpc", params=params) @@ -599,8 +740,12 @@ def mock_config_getint(section: str, key: str): @mock.patch("aurweb.config.getint", side_effect=mock_config_getint) -def test_rpc_ratelimit(getint: mock.MagicMock, client: TestClient, - pipeline: Pipeline, packages: List[Package]): +def test_rpc_ratelimit( + getint: mock.MagicMock, + client: TestClient, + pipeline: Pipeline, + packages: list[Package], +): params = {"v": 5, "type": "suggest-pkgbase", "arg": "big"} for i in range(4): @@ -626,7 +771,7 @@ def test_rpc_ratelimit(getint: mock.MagicMock, client: TestClient, assert response.status_code == int(HTTPStatus.OK) -def test_rpc_etag(client: TestClient, packages: List[Package]): +def test_rpc_etag(client: TestClient, packages: list[Package]): params = {"v": 5, "type": "suggest-pkgbase", "arg": "big"} with client as request: @@ -647,7 +792,7 @@ def test_rpc_search_arg_too_small(client: TestClient): assert response.json().get("error") == "Query arg too small." -def test_rpc_search(client: TestClient, packages: List[Package]): +def test_rpc_search(client: TestClient, packages: list[Package]): params = {"v": 5, "type": "search", "arg": "big"} with client as request: response = request.get("/rpc", params=params) @@ -658,13 +803,14 @@ def test_rpc_search(client: TestClient, packages: List[Package]): result = data.get("results")[0] assert result.get("Name") == packages[0].Name + assert result.get("Submitter") is None # Test the If-None-Match headers. etag = response.headers.get("ETag").strip('"') headers = {"If-None-Match": etag} response = request.get("/rpc", params=params, headers=headers) assert response.status_code == int(HTTPStatus.NOT_MODIFIED) - assert response.content == b'' + assert response.content == b"" # No args on non-m by types return an error. del params["arg"] @@ -673,7 +819,7 @@ def test_rpc_search(client: TestClient, packages: List[Package]): assert response.json().get("error") == "No request type/data specified." -def test_rpc_msearch(client: TestClient, user: User, packages: List[Package]): +def test_rpc_msearch(client: TestClient, user: User, packages: list[Package]): params = {"v": 5, "type": "msearch", "arg": user.Username} with client as request: response = request.get("/rpc", params=params) @@ -682,12 +828,7 @@ def test_rpc_msearch(client: TestClient, user: User, packages: List[Package]): # user1 maintains 4 packages; assert that we got them all. assert data.get("resultcount") == 4 names = list(sorted(r.get("Name") for r in data.get("results"))) - expected_results = [ - "big-chungus", - "chungy-chungus", - "gluggly-chungus", - "other-pkg" - ] + expected_results = ["big-chungus", "chungy-chungus", "gluggly-chungus", "other-pkg"] assert names == expected_results # Search for a non-existent maintainer, giving us zero packages. @@ -704,16 +845,15 @@ def test_rpc_msearch(client: TestClient, user: User, packages: List[Package]): params.pop("arg") response = request.get("/rpc", params=params) data = response.json() - assert data.get("resultcount") == 1 + assert data.get("resultcount") == 2 result = data.get("results")[0] assert result.get("Name") == "big-chungus" -def test_rpc_search_depends(client: TestClient, packages: List[Package], - depends: List[PackageDependency]): - params = { - "v": 5, "type": "search", "by": "depends", "arg": "chungus-depends" - } +def test_rpc_search_depends( + client: TestClient, packages: list[Package], depends: list[PackageDependency] +): + params = {"v": 5, "type": "search", "by": "depends", "arg": "chungus-depends"} with client as request: response = request.get("/rpc", params=params) data = response.json() @@ -722,13 +862,14 @@ def test_rpc_search_depends(client: TestClient, packages: List[Package], assert result.get("Name") == packages[0].Name -def test_rpc_search_makedepends(client: TestClient, packages: List[Package], - depends: List[PackageDependency]): +def test_rpc_search_makedepends( + client: TestClient, packages: list[Package], depends: list[PackageDependency] +): params = { "v": 5, "type": "search", "by": "makedepends", - "arg": "chungus-makedepends" + "arg": "chungus-makedepends", } with client as request: response = request.get("/rpc", params=params) @@ -738,14 +879,10 @@ def test_rpc_search_makedepends(client: TestClient, packages: List[Package], assert result.get("Name") == packages[0].Name -def test_rpc_search_optdepends(client: TestClient, packages: List[Package], - depends: List[PackageDependency]): - params = { - "v": 5, - "type": "search", - "by": "optdepends", - "arg": "chungus-optdepends" - } +def test_rpc_search_optdepends( + client: TestClient, packages: list[Package], depends: list[PackageDependency] +): + params = {"v": 5, "type": "search", "by": "optdepends", "arg": "chungus-optdepends"} with client as request: response = request.get("/rpc", params=params) data = response.json() @@ -754,13 +891,14 @@ def test_rpc_search_optdepends(client: TestClient, packages: List[Package], assert result.get("Name") == packages[0].Name -def test_rpc_search_checkdepends(client: TestClient, packages: List[Package], - depends: List[PackageDependency]): +def test_rpc_search_checkdepends( + client: TestClient, packages: list[Package], depends: list[PackageDependency] +): params = { "v": 5, "type": "search", "by": "checkdepends", - "arg": "chungus-checkdepends" + "arg": "chungus-checkdepends", } with client as request: response = request.get("/rpc", params=params) @@ -770,6 +908,131 @@ def test_rpc_search_checkdepends(client: TestClient, packages: List[Package], assert result.get("Name") == packages[0].Name +def test_rpc_search_provides( + client: TestClient, packages: list[Package], relations: list[PackageRelation] +): + params = {"v": 5, "type": "search", "by": "provides", "arg": "chungus-provides"} + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + assert data.get("resultcount") == 1 + result = data.get("results")[0] + assert result.get("Name") == packages[0].Name + + +def test_rpc_search_provides_self( + client: TestClient, packages: list[Package], relations: list[PackageRelation] +): + params = {"v": 5, "type": "search", "by": "provides", "arg": "big-chungus"} + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + # expected to return "big-chungus" + assert data.get("resultcount") == 1 + result = data.get("results")[0] + assert result.get("Name") == packages[0].Name + + +def test_rpc_search_conflicts( + client: TestClient, packages: list[Package], relations: list[PackageRelation] +): + params = {"v": 5, "type": "search", "by": "conflicts", "arg": "chungus-conflicts"} + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + assert data.get("resultcount") == 1 + result = data.get("results")[0] + assert result.get("Name") == packages[0].Name + + +def test_rpc_search_replaces( + client: TestClient, packages: list[Package], relations: list[PackageRelation] +): + params = {"v": 5, "type": "search", "by": "replaces", "arg": "chungus-replaces"} + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + assert data.get("resultcount") == 1 + result = data.get("results")[0] + assert result.get("Name") == packages[0].Name + + +def test_rpc_search_groups( + client: TestClient, packages: list[Package], depends: list[PackageDependency] +): + params = { + "v": 5, + "type": "search", + "by": "groups", + "arg": "testgroup", + } + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + assert data.get("resultcount") == 1 + result = data.get("results")[0] + assert result.get("Name") == packages[0].Name + + +def test_rpc_search_submitter(client: TestClient, user2: User, packages: list[Package]): + params = {"v": 5, "type": "search", "by": "submitter", "arg": user2.Username} + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + + # user2 submitted 2 packages + assert data.get("resultcount") == 2 + names = list(sorted(r.get("Name") for r in data.get("results"))) + expected_results = ["big-chungus", "chungy-chungus"] + assert names == expected_results + + # Search for a non-existent submitter, giving us zero packages. + params["arg"] = "blah-blah" + response = request.get("/rpc", params=params) + data = response.json() + assert data.get("resultcount") == 0 + + +def test_rpc_search_keywords(client: TestClient, packages: list[Package]): + params = {"v": 5, "type": "search", "by": "keywords", "arg": "big-chungus"} + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + + # should get 2 packages + assert data.get("resultcount") == 1 + names = list(sorted(r.get("Name") for r in data.get("results"))) + expected_results = ["big-chungus"] + assert names == expected_results + + # non-existent search + params["arg"] = "blah-blah" + response = request.get("/rpc", params=params) + data = response.json() + assert data.get("resultcount") == 0 + + +def test_rpc_search_comaintainers( + client: TestClient, user2: User, packages: list[Package] +): + params = {"v": 5, "type": "search", "by": "comaintainers", "arg": user2.Username} + with client as request: + response = request.get("/rpc", params=params) + data = response.json() + + # should get 1 package + assert data.get("resultcount") == 1 + names = list(sorted(r.get("Name") for r in data.get("results"))) + expected_results = ["big-chungus"] + assert names == expected_results + + # non-existent search + params["arg"] = "blah-blah" + response = request.get("/rpc", params=params) + data = response.json() + assert data.get("resultcount") == 0 + + def test_rpc_incorrect_by(client: TestClient): params = {"v": 5, "type": "search", "by": "fake", "arg": "big"} with client as request: @@ -778,21 +1041,16 @@ def test_rpc_incorrect_by(client: TestClient): def test_rpc_jsonp_callback(client: TestClient): - """ Test the callback parameter. + """Test the callback parameter. For end-to-end verification, the `examples/jsonp.html` file can be used to submit jsonp callback requests to the RPC. """ - params = { - "v": 5, - "type": "search", - "arg": "big", - "callback": "jsonCallback" - } + params = {"v": 5, "type": "search", "arg": "big", "callback": "jsonCallback"} with client as request: response = request.get("/rpc", params=params) assert response.headers.get("content-type") == "text/javascript" - assert re.search(r'^/\*\*/jsonCallback\(.*\)$', response.text) is not None + assert re.search(r"^/\*\*/jsonCallback\(.*\)$", response.text) is not None # Test an invalid callback name; we get an application/json error. params["callback"] = "jsonCallback!" @@ -802,21 +1060,15 @@ def test_rpc_jsonp_callback(client: TestClient): assert response.json().get("error") == "Invalid callback name." -def test_rpc_post(client: TestClient, packages: List[Package]): - data = { - "v": 5, - "type": "info", - "arg": "big-chungus", - "arg[]": ["chungy-chungus"] - } +def test_rpc_post(client: TestClient, packages: list[Package]): + data = {"v": 5, "type": "info", "arg": "big-chungus", "arg[]": ["chungy-chungus"]} with client as request: resp = request.post("/rpc", data=data) assert resp.status_code == int(HTTPStatus.OK) assert resp.json().get("resultcount") == 2 -def test_rpc_too_many_search_results(client: TestClient, - packages: List[Package]): +def test_rpc_too_many_search_results(client: TestClient, packages: list[Package]): config_getint = config.getint def mock_config(section: str, key: str): @@ -831,16 +1083,24 @@ def test_rpc_too_many_search_results(client: TestClient, assert resp.json().get("error") == "Too many package results." -def test_rpc_too_many_info_results(client: TestClient, packages: List[Package]): +def test_rpc_too_many_info_results(client: TestClient, packages: list[Package]): # Make many of these packages depend and rely on each other. # This way, we can test to see that the exceeded limit stays true # regardless of the number of related records. with db.begin(): for i in range(len(packages) - 1): - db.create(PackageDependency, DepTypeID=DEPENDS_ID, - Package=packages[i], DepName=packages[i + 1].Name) - db.create(PackageRelation, RelTypeID=PROVIDES_ID, - Package=packages[i], RelName=packages[i + 1].Name) + db.create( + PackageDependency, + DepTypeID=DEPENDS_ID, + Package=packages[i], + DepName=packages[i + 1].Name, + ) + db.create( + PackageRelation, + RelTypeID=PROVIDES_ID, + Package=packages[i], + RelName=packages[i + 1].Name, + ) config_getint = config.getint @@ -854,3 +1114,126 @@ def test_rpc_too_many_info_results(client: TestClient, packages: List[Package]): with client as request: resp = request.get("/rpc", params=params) assert resp.json().get("error") == "Too many package results." + + +def test_rpc_openapi_info(client: TestClient, packages: list[Package]): + pkgname = packages[0].Name + + with client as request: + endp = f"/rpc/v5/info/{pkgname}" + resp = request.get(endp) + assert resp.status_code == HTTPStatus.OK + + data = resp.json() + assert data.get("resultcount") == 1 + + +def test_rpc_openapi_multiinfo(client: TestClient, packages: list[Package]): + pkgname = packages[0].Name + + with client as request: + endp = "/rpc/v5/info" + resp = request.get(endp, params={"arg[]": [pkgname]}) + assert resp.status_code == HTTPStatus.OK + + data = resp.json() + assert data.get("resultcount") == 1 + + +def test_rpc_openapi_multiinfo_post(client: TestClient, packages: list[Package]): + pkgname = packages[0].Name + + with client as request: + endp = "/rpc/v5/info" + resp = request.post(endp, json={"arg": [pkgname]}) + assert resp.status_code == HTTPStatus.OK + + data = resp.json() + assert data.get("resultcount") == 1 + + +def test_rpc_openapi_multiinfo_post_bad_request( + client: TestClient, packages: list[Package] +): + pkgname = packages[0].Name + + with client as request: + endp = "/rpc/v5/info" + resp = request.post(endp, json={"arg": pkgname}) + assert resp.status_code == HTTPStatus.BAD_REQUEST + + data = resp.json() + expected = "the 'arg' parameter must be of array type" + assert data.get("error") == expected + + +def test_rpc_openapi_search_arg(client: TestClient, packages: list[Package]): + pkgname = packages[0].Name + + with client as request: + endp = f"/rpc/v5/search/{pkgname}" + resp = request.get(endp) + assert resp.status_code == HTTPStatus.OK + + data = resp.json() + assert data.get("resultcount") == 1 + + +def test_rpc_openapi_search(client: TestClient, packages: list[Package]): + pkgname = packages[0].Name + + with client as request: + endp = "/rpc/v5/search" + resp = request.get(endp, params={"arg": pkgname}) + assert resp.status_code == HTTPStatus.OK + + data = resp.json() + assert data.get("resultcount") == 1 + + +def test_rpc_openapi_search_post(client: TestClient, packages: list[Package]): + pkgname = packages[0].Name + + with client as request: + endp = "/rpc/v5/search" + resp = request.post(endp, json={"arg": pkgname}) + assert resp.status_code == HTTPStatus.OK + + data = resp.json() + assert data.get("resultcount") == 1 + + +def test_rpc_openapi_search_post_bad_request(client: TestClient): + # Test by parameter + with client as request: + endp = "/rpc/v5/search" + resp = request.post(endp, json={"by": 1}) + assert resp.status_code == HTTPStatus.BAD_REQUEST + data = resp.json() + expected = "the 'by' parameter must be of string type" + assert data.get("error") == expected + + # Test arg parameter + with client as request: + endp = "/rpc/v5/search" + resp = request.post(endp, json={"arg": ["a", "list"]}) + assert resp.status_code == HTTPStatus.BAD_REQUEST + data = resp.json() + expected = "the 'arg' parameter must be of string type" + assert data.get("error") == expected + + +def test_rpc_openapi_suggest(client: TestClient, packages: list[Package]): + suggestions = { + "big": ["big-chungus"], + "chungy": ["chungy-chungus"], + } + + for term, expected in suggestions.items(): + with client as request: + endp = f"/rpc/v5/suggest/{term}" + resp = request.get(endp) + assert resp.status_code == HTTPStatus.OK + + data = resp.json() + assert data == expected diff --git a/test/test_rss.py b/test/test_rss.py index cef6a46f..d227a183 100644 --- a/test/test_rss.py +++ b/test/test_rss.py @@ -2,17 +2,16 @@ from http import HTTPStatus import lxml.etree import pytest - from fastapi.testclient import TestClient -from aurweb import db, logging, time +from aurweb import aur_logging, db, time from aurweb.asgi import app from aurweb.models.account_type import AccountType from aurweb.models.package import Package from aurweb.models.package_base import PackageBase from aurweb.models.user import User -logger = logging.get_logger(__name__) +logger = aur_logging.get_logger(__name__) @pytest.fixture(autouse=True) @@ -27,13 +26,15 @@ def client(): @pytest.fixture def user(): - account_type = db.query(AccountType, - AccountType.AccountType == "User").first() - yield db.create(User, Username="test", - Email="test@example.org", - RealName="Test User", - Passwd="testPassword", - AccountType=account_type) + account_type = db.query(AccountType, AccountType.AccountType == "User").first() + yield db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountType=account_type, + ) @pytest.fixture @@ -45,8 +46,12 @@ def packages(user): with db.begin(): for i in range(101): pkgbase = db.create( - PackageBase, Maintainer=user, Name=f"test-package-{i}", - SubmittedTS=(now + i), ModifiedTS=(now + i)) + PackageBase, + Maintainer=user, + Name=f"test-package-{i}", + SubmittedTS=(now + i), + ModifiedTS=(now + i), + ) pkg = db.create(Package, Name=pkgbase.Name, PackageBase=pkgbase) pkgs.append(pkg) yield pkgs @@ -64,6 +69,7 @@ def test_rss(client, user, packages): # Test that the RSS we got is sorted by descending SubmittedTS. def key_(pkg): return pkg.PackageBase.SubmittedTS + packages = list(reversed(sorted(packages, key=key_))) # Just take the first 100. @@ -74,7 +80,7 @@ def test_rss(client, user, packages): assert len(items) == 100 for i, item in enumerate(items): - title = next(iter(item.xpath('./title'))) + title = next(iter(item.xpath("./title"))) logger.debug(f"title: '{title.text}' vs name: '{packages[i].Name}'") assert title.text == packages[i].Name @@ -87,6 +93,7 @@ def test_rss_modified(client, user, packages): # Test that the RSS we got is sorted by descending SubmittedTS. def key_(pkg): return pkg.PackageBase.ModifiedTS + packages = list(reversed(sorted(packages, key=key_))) # Just take the first 100. @@ -97,6 +104,6 @@ def test_rss_modified(client, user, packages): assert len(items) == 100 for i, item in enumerate(items): - title = next(iter(item.xpath('./title'))) + title = next(iter(item.xpath("./title"))) logger.debug(f"title: '{title.text}' vs name: '{packages[i].Name}'") assert title.text == packages[i].Name diff --git a/test/test_session.py b/test/test_session.py index edae57f9..f1af9613 100644 --- a/test/test_session.py +++ b/test/test_session.py @@ -1,8 +1,8 @@ """ Test our Session model. """ + from unittest import mock import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db, time @@ -19,17 +19,23 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - ResetKey="testReset", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + ResetKey="testReset", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @pytest.fixture def session(user: User) -> Session: with db.begin(): - session = db.create(Session, User=user, SessionID="testSession", - LastUpdateTS=time.utcnow()) + session = db.create( + Session, User=user, SessionID="testSession", LastUpdateTS=time.utcnow() + ) yield session @@ -39,15 +45,21 @@ def test_session(user: User, session: Session): def test_session_cs(): - """ Test case sensitivity of the database table. """ + """Test case sensitivity of the database table.""" with db.begin(): - user2 = db.create(User, Username="test2", Email="test2@example.org", - ResetKey="testReset2", Passwd="testPassword", - AccountTypeID=USER_ID) + user2 = db.create( + User, + Username="test2", + Email="test2@example.org", + ResetKey="testReset2", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) with db.begin(): - session_cs = db.create(Session, User=user2, SessionID="TESTSESSION", - LastUpdateTS=time.utcnow()) + session_cs = db.create( + Session, User=user2, SessionID="TESTSESSION", LastUpdateTS=time.utcnow() + ) assert session_cs.SessionID == "TESTSESSION" assert session_cs.SessionID != "testSession" diff --git a/test/test_spawn.py b/test/test_spawn.py index 195eb897..c57c9b52 100644 --- a/test/test_spawn.py +++ b/test/test_spawn.py @@ -1,6 +1,5 @@ import os import tempfile - from typing import Tuple from unittest import mock @@ -9,28 +8,22 @@ import pytest import aurweb.config import aurweb.spawn -from aurweb.exceptions import AurwebException - # Some os.environ overrides we use in this suite. -TEST_ENVIRONMENT = { - "PHP_NGINX_PORT": "8001", - "FASTAPI_NGINX_PORT": "8002" -} +TEST_ENVIRONMENT = {"FASTAPI_NGINX_PORT": "8002"} class FakeProcess: - """ Fake a subprocess.Popen return object. """ + """Fake a subprocess.Popen return object.""" returncode = 0 - stdout = b'' - stderr = b'' + stdout = b"" + stderr = b"" def __init__(self, *args, **kwargs): - """ We need this constructor to remain compatible with Popen. """ - pass + """We need this constructor to remain compatible with Popen.""" def communicate(self) -> Tuple[bytes, bytes]: - return (self.stdout, self.stderr) + return self.stdout, self.stderr def terminate(self) -> None: raise Exception("Fake termination.") @@ -40,10 +33,9 @@ class FakeProcess: class MockFakeProcess: - """ FakeProcess construction helper to be used in mocks. """ + """FakeProcess construction helper to be used in mocks.""" - def __init__(self, return_code: int = 0, stdout: bytes = b'', - stderr: bytes = b''): + def __init__(self, return_code: int = 0, stdout: bytes = b"", stderr: bytes = b""): self.returncode = return_code self.stdout = stdout self.stderr = stderr @@ -56,34 +48,6 @@ class MockFakeProcess: return proc -@mock.patch("aurweb.spawn.PHP_BINARY", "does-not-exist") -def test_spawn(): - match = r"^Unable to locate the '.*' executable\.$" - with pytest.raises(AurwebException, match=match): - aurweb.spawn.validate_php_config() - - -@mock.patch("subprocess.Popen", side_effect=MockFakeProcess(1).process) -def test_spawn_non_zero_php_binary(fake_process: FakeProcess): - match = r"^Received non-zero error code.*$" - with pytest.raises(AssertionError, match=match): - aurweb.spawn.validate_php_config() - - -def test_spawn_missing_modules(): - side_effect = MockFakeProcess(stdout=b"pdo_sqlite").process - with mock.patch("subprocess.Popen", side_effect=side_effect): - match = r"PHP does not have the 'pdo_mysql' module enabled\.$" - with pytest.raises(AurwebException, match=match): - aurweb.spawn.validate_php_config() - - side_effect = MockFakeProcess(stdout=b"pdo_mysql").process - with mock.patch("subprocess.Popen", side_effect=side_effect): - match = r"PHP does not have the 'pdo_sqlite' module enabled\.$" - with pytest.raises(AurwebException, match=match): - aurweb.spawn.validate_php_config() - - @mock.patch.dict("os.environ", TEST_ENVIRONMENT) def test_spawn_generate_nginx_config(): ctx = tempfile.TemporaryDirectory() @@ -93,15 +57,11 @@ def test_spawn_generate_nginx_config(): with open(nginx_config_path) as f: nginx_config = f.read().rstrip() - php_address = aurweb.config.get("php", "bind_address") - php_host = php_address.split(":")[0] fastapi_address = aurweb.config.get("fastapi", "bind_address") fastapi_host = fastapi_address.split(":")[0] expected_content = [ - f'listen {php_host}:{TEST_ENVIRONMENT.get("PHP_NGINX_PORT")}', - f"proxy_pass http://{php_address}", f'listen {fastapi_host}:{TEST_ENVIRONMENT.get("FASTAPI_NGINX_PORT")}', - f"proxy_pass http://{fastapi_address}" + f"proxy_pass http://{fastapi_address}", ] for expected in expected_content: assert expected in nginx_config diff --git a/test/test_ssh_pub_key.py b/test/test_ssh_pub_key.py index 93298a11..1a586800 100644 --- a/test/test_ssh_pub_key.py +++ b/test/test_ssh_pub_key.py @@ -27,18 +27,23 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user @pytest.fixture def pubkey(user: User) -> SSHPubKey: with db.begin(): - pubkey = db.create(SSHPubKey, User=user, - Fingerprint="testFingerprint", - PubKey="testPubKey") + pubkey = db.create( + SSHPubKey, User=user, Fingerprint="testFingerprint", PubKey="testPubKey" + ) yield pubkey @@ -50,11 +55,11 @@ def test_pubkey(user: User, pubkey: SSHPubKey): def test_pubkey_cs(user: User): - """ Test case sensitivity of the database table. """ + """Test case sensitivity of the database table.""" with db.begin(): - pubkey_cs = db.create(SSHPubKey, User=user, - Fingerprint="TESTFINGERPRINT", - PubKey="TESTPUBKEY") + pubkey_cs = db.create( + SSHPubKey, User=user, Fingerprint="TESTFINGERPRINT", PubKey="TESTPUBKEY" + ) assert pubkey_cs.Fingerprint == "TESTFINGERPRINT" assert pubkey_cs.Fingerprint != "testFingerprint" diff --git a/test/test_statistics.py b/test/test_statistics.py new file mode 100644 index 00000000..4859a2ce --- /dev/null +++ b/test/test_statistics.py @@ -0,0 +1,159 @@ +import pytest +from prometheus_client import REGISTRY, generate_latest + +from aurweb import cache, db, time +from aurweb.models import Package, PackageBase, PackageRequest +from aurweb.models.account_type import PACKAGE_MAINTAINER_ID, USER_ID +from aurweb.models.package_request import ( + ACCEPTED_ID, + CLOSED_ID, + PENDING_ID, + REJECTED_ID, +) +from aurweb.models.request_type import DELETION_ID, ORPHAN_ID +from aurweb.models.user import User +from aurweb.statistics import Statistics, update_prometheus_metrics + + +@pytest.fixture(autouse=True) +def setup(db_test, prometheus_test): + return + + +@pytest.fixture(autouse=True) +def clear_fakeredis_cache(): + cache._redis.flushall() + + +@pytest.fixture +def test_data(): + # Create some test data (users and packages) + with db.begin(): + for i in range(10): + user = db.create( + User, + Username=f"test{i}", + Email=f"test{i}@example.org", + RealName=f"Test User {i}", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) + + now = time.utcnow() + old = now - 60 * 60 * 24 * 8 # 8 days + older = now - 60 * 60 * 24 * 400 # 400 days + + pkgbase = db.create( + PackageBase, + Name=f"test-package{i}", + Maintainer=user, + SubmittedTS=old, + ModifiedTS=now, + ) + db.create(Package, PackageBase=pkgbase, Name=pkgbase.Name) + pkgreq = db.create( + PackageRequest, + ReqTypeID=ORPHAN_ID, + User=user, + PackageBase=pkgbase, + PackageBaseName=pkgbase.Name, + RequestTS=now, + Comments=str(), + ClosureComment=str(), + ) + + # Modify some data to get some variances for our counters + if i == 1: + user.AccountTypeID = PACKAGE_MAINTAINER_ID + pkgbase.Maintainer = None + pkgbase.SubmittedTS = now + pkgreq.Status = PENDING_ID + pkgreq.ReqTypeID = DELETION_ID + + if i == 2: + pkgbase.SubmittedTS = older + pkgreq.Status = ACCEPTED_ID + + if i == 3: + pkgbase.SubmittedTS = older + pkgbase.ModifiedTS = old + pkgreq.Status = CLOSED_ID + + if i == 4: + pkgreq.Status = REJECTED_ID + yield + + +@pytest.fixture +def stats() -> Statistics: + yield Statistics() + + +@pytest.mark.parametrize( + "counter, expected", + [ + ("package_count", 10), + ("orphan_count", 1), + ("seven_days_old_added", 1), + ("seven_days_old_updated", 8), + ("year_old_updated", 9), + ("never_updated", 1), + ("user_count", 10), + ("package_maintainer_count", 1), + ("regular_user_count", 9), + ("updated_packages", 9), + ("total_requests", 10), + ("pending_requests", 7), + ("closed_requests", 1), + ("accepted_requests", 1), + ("rejected_requests", 1), + ("nonsense", -1), + ], +) +def test_get_count(stats: Statistics, test_data, counter: str, expected: int): + assert stats.get_count(counter) == expected + + +def test_get_count_change(stats: Statistics, test_data): + pkgs_before = stats.get_count("package_count") + pms_before = stats.get_count("package_maintainer_count") + + assert pkgs_before == 10 + assert pms_before == 1 + + # Let's delete a package and promote a user to TU + with db.begin(): + pkgbase = db.query(PackageBase).first() + db.delete(pkgbase) + + user = db.query(User).filter(User.AccountTypeID == USER_ID).first() + user.AccountTypeID = PACKAGE_MAINTAINER_ID + + # Values should end up in (fake) redis cache so they should be the same + assert stats.get_count("package_count") == pkgs_before + assert stats.get_count("package_maintainer_count") == pms_before + + # Let's clear the cache and check again + cache._redis.flushall() + assert stats.get_count("package_count") != pkgs_before + assert stats.get_count("package_maintainer_count") != pms_before + + +def test_update_prometheus_metrics(test_data): + metrics = str(generate_latest(REGISTRY)) + + assert "aur_users{" not in metrics + assert "aur_packages{" not in metrics + assert "aur_requests{" not in metrics + + # Let's update our metrics. We should find our gauges now + update_prometheus_metrics() + metrics = str(generate_latest(REGISTRY)) + + assert 'aur_users{type="user"} 9.0' in metrics + assert 'aur_packages{state="updated"} 9.0' in metrics + assert 'aur_requests{status="Pending",type="orphan"} 6.0' in metrics + assert 'aur_requests{status="Closed",type="orphan"} 1.0' in metrics + assert 'aur_requests{status="Accepted",type="orphan"} 1.0' in metrics + assert 'aur_requests{status="Rejected",type="orphan"} 1.0' in metrics + assert 'aur_requests{status="Pending",type="deletion"} 1.0' in metrics diff --git a/test/test_templates.py b/test/test_templates.py index 7d6b585c..18a1f2b8 100644 --- a/test/test_templates.py +++ b/test/test_templates.py @@ -1,21 +1,27 @@ import re - -from typing import Any, Dict +from typing import Any import pytest import aurweb.filters # noqa: F401 - from aurweb import config, db, templates, time -from aurweb.filters import as_timezone, number_format -from aurweb.filters import timestamp_to_datetime as to_dt +from aurweb.filters import as_timezone, number_format, timestamp_to_datetime as to_dt from aurweb.models import Package, PackageBase, User from aurweb.models.account_type import USER_ID +from aurweb.models.group import Group from aurweb.models.license import License +from aurweb.models.package_base import popularity +from aurweb.models.package_group import PackageGroup from aurweb.models.package_license import PackageLicense from aurweb.models.package_relation import PackageRelation from aurweb.models.relation_type import PROVIDES_ID, REPLACES_ID -from aurweb.templates import base_template, make_context, register_filter, register_function +from aurweb.templates import ( + base_template, + make_context, + make_variable_context, + register_filter, + register_function, +) from aurweb.testing.html import parse_root from aurweb.testing.requests import Request @@ -35,19 +41,20 @@ def function(): def create_user(username: str) -> User: with db.begin(): - user = db.create(User, Username=username, - Email=f"{username}@example.org", - Passwd="testPassword", - AccountTypeID=USER_ID) + user = db.create( + User, + Username=username, + Email=f"{username}@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) return user -def create_pkgrel(package: Package, reltype_id: int, relname: str) \ - -> PackageRelation: - return db.create(PackageRelation, - Package=package, - RelTypeID=reltype_id, - RelName=relname) +def create_pkgrel(package: Package, reltype_id: int, relname: str) -> PackageRelation: + return db.create( + PackageRelation, Package=package, RelTypeID=reltype_id, RelName=relname + ) @pytest.fixture @@ -60,8 +67,13 @@ def user(db_test) -> User: def pkgbase(user: User) -> PackageBase: now = time.utcnow() with db.begin(): - pkgbase = db.create(PackageBase, Name="test-pkg", Maintainer=user, - SubmittedTS=now, ModifiedTS=now) + pkgbase = db.create( + PackageBase, + Name="test-pkg", + Maintainer=user, + SubmittedTS=now, + ModifiedTS=now, + ) yield pkgbase @@ -78,10 +90,17 @@ def create_license(pkg: Package, license_name: str) -> PackageLicense: return pkglic +def create_group(pkg: Package, group_name: str) -> PackageGroup: + grp = db.create(Group, Name=group_name) + pkggrp = db.create(PackageGroup, Group=grp, Package=pkg) + return pkggrp + + def test_register_function_exists_key_error(): - """ Most instances of register_filter are tested through module - imports or template renders, so we only test failures here. """ + """Most instances of register_filter are tested through module + imports or template renders, so we only test failures here.""" with pytest.raises(KeyError): + @register_function("function") def some_func(): pass @@ -93,8 +112,9 @@ def test_commit_hash(): commit_hash = "abcdefg" long_commit_hash = commit_hash + "1234567" - def config_get_with_fallback(section: str, option: str, - fallback: str = None) -> str: + def config_get_with_fallback( + section: str, option: str, fallback: str = None + ) -> str: if section == "devel" and option == "commit_hash": return long_commit_hash return config.original_get_with_fallback(section, option, fallback) @@ -126,7 +146,7 @@ def test_commit_hash(): assert commit_hash not in render -def pager_context(num_packages: int) -> Dict[str, Any]: +def pager_context(num_packages: int) -> dict[str, Any]: return { "request": Request(), "singular": "%d package found.", @@ -134,12 +154,12 @@ def pager_context(num_packages: int) -> Dict[str, Any]: "prefix": "/packages", "total": num_packages, "O": 0, - "PP": 50 + "PP": 50, } def test_pager_no_results(): - """ Test the pager partial with no results. """ + """Test the pager partial with no results.""" num_packages = 0 context = pager_context(num_packages) body = base_template("partials/pager.html").render(context) @@ -151,7 +171,7 @@ def test_pager_no_results(): def test_pager(): - """ Test the pager partial with two pages of results. """ + """Test the pager partial with two pages of results.""" num_packages = 100 context = pager_context(num_packages) body = base_template("partials/pager.html").render(context) @@ -207,6 +227,15 @@ def check_package_details(content: str, pkg: Package) -> None: else: assert "Licenses" not in content + groups = pkg.package_groups.all() + if groups: + i += 1 + expected = ", ".join([p.Group.Name for p in groups]) + group_markup = rows[i].xpath("./td")[0] + assert group_markup.text.strip() == expected + else: + assert "Groups" not in content + provides = pkg.package_relations.filter( PackageRelation.RelTypeID == PROVIDES_ID ).all() @@ -274,16 +303,21 @@ def check_package_details(content: str, pkg: Package) -> None: def test_package_details(user: User, package: Package): - """ Test package details with most fields populated, but not all. """ + """Test package details with most fields populated, but not all.""" request = Request(user=user, authenticated=True) context = make_context(request, "Test Details") - context.update({ - "request": request, - "git_clone_uri_anon": GIT_CLONE_URI_ANON, - "git_clone_uri_priv": GIT_CLONE_URI_PRIV, - "pkgbase": package.PackageBase, - "pkg": package - }) + + context.update( + { + "request": request, + "git_clone_uri_anon": GIT_CLONE_URI_ANON, + "git_clone_uri_priv": GIT_CLONE_URI_PRIV, + "pkgbase": package.PackageBase, + "popularity": popularity(package.PackageBase, time.utcnow()), + "package": package, + "comaintainers": [], + } + ) base = base_template("partials/packages/details.html") body = base.render(context, show_package_details=True) @@ -291,7 +325,7 @@ def test_package_details(user: User, package: Package): def test_package_details_filled(user: User, package: Package): - """ Test package details with all fields populated. """ + """Test package details with all fields populated.""" pkgbase = package.PackageBase with db.begin(): @@ -302,6 +336,10 @@ def test_package_details_filled(user: User, package: Package): create_license(package, "TPL") # Testing Public License create_license(package, "TPL2") # Testing Public License 2 + # Create two groups. + create_group(package, "GRP") + create_group(package, "GRP2") + # Add provides. create_pkgrel(package, PROVIDES_ID, "test-provider") @@ -310,19 +348,55 @@ def test_package_details_filled(user: User, package: Package): request = Request(user=user, authenticated=True) context = make_context(request, "Test Details") - context.update({ - "request": request, - "git_clone_uri_anon": GIT_CLONE_URI_ANON, - "git_clone_uri_priv": GIT_CLONE_URI_PRIV, - "pkgbase": package.PackageBase, - "pkg": package, - "licenses": package.package_licenses, - "provides": package.package_relations.filter( - PackageRelation.RelTypeID == PROVIDES_ID), - "replaces": package.package_relations.filter( - PackageRelation.RelTypeID == REPLACES_ID), - }) + context.update( + { + "request": request, + "git_clone_uri_anon": GIT_CLONE_URI_ANON, + "git_clone_uri_priv": GIT_CLONE_URI_PRIV, + "pkgbase": package.PackageBase, + "popularity": popularity(package.PackageBase, time.utcnow()), + "package": package, + "comaintainers": [], + "licenses": package.package_licenses, + "groups": package.package_groups, + "provides": package.package_relations.filter( + PackageRelation.RelTypeID == PROVIDES_ID + ), + "replaces": package.package_relations.filter( + PackageRelation.RelTypeID == REPLACES_ID + ), + } + ) base = base_template("partials/packages/details.html") body = base.render(context, show_package_details=True) check_package_details(body, package) + + +def test_make_context_timezone(user: User, package: Package): + request = Request( + user=user, authenticated=True, url="/packages/test?timezone=foobar" + ) + context = make_context(request, "Test Details") + assert context["timezone"] in time.SUPPORTED_TIMEZONES + + +@pytest.mark.asyncio +async def test_make_variable_context_timezone(user: User, package: Package): + request = Request( + user=user, authenticated=True, url="/packages/test?timezone=foobar" + ) + context = await make_variable_context( + request, "Test Details", next="/packages/test" + ) + assert context["timezone"] in time.SUPPORTED_TIMEZONES + + +@pytest.mark.asyncio +async def test_make_variable_context_params(): + request = Request(url="/test", query_params={"request": "test", "x": "test"}) + context = await make_variable_context(request, "Test") + + # make sure we can't override our Request object with a query parameter + assert context["request"] != "test" + assert context["x"] == "test" diff --git a/test/test_term.py b/test/test_term.py index bfa73a76..4b608a9a 100644 --- a/test/test_term.py +++ b/test/test_term.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy.exc import IntegrityError from aurweb import db @@ -13,8 +12,9 @@ def setup(db_test): def test_term_creation(): with db.begin(): - term = db.create(Term, Description="Term description", - URL="https://fake_url.io") + term = db.create( + Term, Description="Term description", URL="https://fake_url.io" + ) assert bool(term.ID) assert term.Description == "Term description" assert term.URL == "https://fake_url.io" diff --git a/test/test_time.py b/test/test_time.py index 2134d217..45328717 100644 --- a/test/test_time.py +++ b/test/test_time.py @@ -1,5 +1,4 @@ import aurweb.config - from aurweb.testing.requests import Request from aurweb.time import get_request_timezone, tz_offset @@ -16,18 +15,22 @@ def test_tz_offset_mst(): def test_request_timezone(): request = Request() - tz = get_request_timezone(request) - assert tz == aurweb.config.get("options", "default_timezone") + # Default timezone + dtz = aurweb.config.get("options", "default_timezone") + assert get_request_timezone(request) == dtz -def test_authenticated_request_timezone(): - # Modify a fake request to be authenticated with the - # America/Los_Angeles timezone. - request = Request() + # Timezone from query params + request.query_params = {"timezone": "Europe/Berlin"} + assert get_request_timezone(request) == "Europe/Berlin" + + # Timezone from authenticated user. + request.query_params = {} request.user.authenticated = True request.user.Timezone = "America/Los_Angeles" + assert get_request_timezone(request) == "America/Los_Angeles" - # Get the request's timezone, it should be America/Los_Angeles. - tz = get_request_timezone(request) - assert tz == request.user.Timezone - assert tz == "America/Los_Angeles" + # Timezone from authenticated user with query param + # Query param should have precedence + request.query_params = {"timezone": "Europe/Berlin"} + assert get_request_timezone(request) == "Europe/Berlin" diff --git a/test/test_tu_vote.py b/test/test_tu_vote.py deleted file mode 100644 index 91d73ecb..00000000 --- a/test/test_tu_vote.py +++ /dev/null @@ -1,54 +0,0 @@ -import pytest - -from sqlalchemy.exc import IntegrityError - -from aurweb import db, time -from aurweb.models.account_type import TRUSTED_USER_ID -from aurweb.models.tu_vote import TUVote -from aurweb.models.tu_voteinfo import TUVoteInfo -from aurweb.models.user import User - - -@pytest.fixture(autouse=True) -def setup(db_test): - return - - -@pytest.fixture -def user() -> User: - with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=TRUSTED_USER_ID) - yield user - - -@pytest.fixture -def tu_voteinfo(user: User) -> TUVoteInfo: - ts = time.utcnow() - with db.begin(): - tu_voteinfo = db.create(TUVoteInfo, Agenda="Blah blah.", - User=user.Username, - Submitted=ts, End=ts + 5, - Quorum=0.5, Submitter=user) - yield tu_voteinfo - - -def test_tu_vote_creation(user: User, tu_voteinfo: TUVoteInfo): - with db.begin(): - tu_vote = db.create(TUVote, User=user, VoteInfo=tu_voteinfo) - - assert tu_vote.VoteInfo == tu_voteinfo - assert tu_vote.User == user - assert tu_vote in user.tu_votes - assert tu_vote in tu_voteinfo.tu_votes - - -def test_tu_vote_null_user_raises_exception(tu_voteinfo: TUVoteInfo): - with pytest.raises(IntegrityError): - TUVote(VoteInfo=tu_voteinfo) - - -def test_tu_vote_null_voteinfo_raises_exception(user: User): - with pytest.raises(IntegrityError): - TUVote(User=user) diff --git a/test/test_tu_voteinfo.py b/test/test_tu_voteinfo.py deleted file mode 100644 index 17226048..00000000 --- a/test/test_tu_voteinfo.py +++ /dev/null @@ -1,148 +0,0 @@ -import pytest - -from sqlalchemy.exc import IntegrityError - -from aurweb import db, time -from aurweb.db import create, rollback -from aurweb.models.account_type import TRUSTED_USER_ID -from aurweb.models.tu_voteinfo import TUVoteInfo -from aurweb.models.user import User - - -@pytest.fixture(autouse=True) -def setup(db_test): - return - - -@pytest.fixture -def user() -> User: - with db.begin(): - user = create(User, Username="test", Email="test@example.org", - RealName="Test User", Passwd="testPassword", - AccountTypeID=TRUSTED_USER_ID) - yield user - - -def test_tu_voteinfo_creation(user: User): - ts = time.utcnow() - with db.begin(): - tu_voteinfo = create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - Submitted=ts, End=ts + 5, - Quorum=0.5, - Submitter=user) - assert bool(tu_voteinfo.ID) - assert tu_voteinfo.Agenda == "Blah blah." - assert tu_voteinfo.User == user.Username - assert tu_voteinfo.Submitted == ts - assert tu_voteinfo.End == ts + 5 - assert tu_voteinfo.Quorum == 0.5 - assert tu_voteinfo.Submitter == user - assert tu_voteinfo.Yes == 0 - assert tu_voteinfo.No == 0 - assert tu_voteinfo.Abstain == 0 - assert tu_voteinfo.ActiveTUs == 0 - - assert tu_voteinfo in user.tu_voteinfo_set - - -def test_tu_voteinfo_is_running(user: User): - ts = time.utcnow() - with db.begin(): - tu_voteinfo = create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - Submitted=ts, End=ts + 1000, - Quorum=0.5, - Submitter=user) - assert tu_voteinfo.is_running() is True - - with db.begin(): - tu_voteinfo.End = ts - 5 - assert tu_voteinfo.is_running() is False - - -def test_tu_voteinfo_total_votes(user: User): - ts = time.utcnow() - with db.begin(): - tu_voteinfo = create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - Submitted=ts, End=ts + 1000, - Quorum=0.5, - Submitter=user) - - tu_voteinfo.Yes = 1 - tu_voteinfo.No = 3 - tu_voteinfo.Abstain = 5 - - # total_votes() should be the sum of Yes, No and Abstain: 1 + 3 + 5 = 9. - assert tu_voteinfo.total_votes() == 9 - - -def test_tu_voteinfo_null_submitter_raises(user: User): - with pytest.raises(IntegrityError): - with db.begin(): - create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - Submitted=0, End=0, - Quorum=0.50) - rollback() - - -def test_tu_voteinfo_null_agenda_raises(user: User): - with pytest.raises(IntegrityError): - with db.begin(): - create(TUVoteInfo, - User=user.Username, - Submitted=0, End=0, - Quorum=0.50, - Submitter=user) - rollback() - - -def test_tu_voteinfo_null_user_raises(user: User): - with pytest.raises(IntegrityError): - with db.begin(): - create(TUVoteInfo, - Agenda="Blah blah.", - Submitted=0, End=0, - Quorum=0.50, - Submitter=user) - rollback() - - -def test_tu_voteinfo_null_submitted_raises(user: User): - with pytest.raises(IntegrityError): - with db.begin(): - create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - End=0, - Quorum=0.50, - Submitter=user) - rollback() - - -def test_tu_voteinfo_null_end_raises(user: User): - with pytest.raises(IntegrityError): - with db.begin(): - create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - Submitted=0, - Quorum=0.50, - Submitter=user) - rollback() - - -def test_tu_voteinfo_null_quorum_default(user: User): - with db.begin(): - vi = create(TUVoteInfo, - Agenda="Blah blah.", - User=user.Username, - Submitted=0, End=0, - Submitter=user) - assert vi.Quorum == 0 diff --git a/test/test_tuvotereminder.py b/test/test_tuvotereminder.py deleted file mode 100644 index a54c52a4..00000000 --- a/test/test_tuvotereminder.py +++ /dev/null @@ -1,101 +0,0 @@ -from typing import Tuple - -import pytest - -from aurweb import config, db, time -from aurweb.models import TUVote, TUVoteInfo, User -from aurweb.models.account_type import TRUSTED_USER_ID -from aurweb.scripts import tuvotereminder as reminder -from aurweb.testing.email import Email - -aur_location = config.get("options", "aur_location") - - -def create_vote(user: User, voteinfo: TUVoteInfo) -> TUVote: - with db.begin(): - vote = db.create(TUVote, User=user, VoteID=voteinfo.ID) - return vote - - -def create_user(username: str, type_id: int): - with db.begin(): - user = db.create(User, AccountTypeID=type_id, Username=username, - Email=f"{username}@example.org", Passwd=str()) - return user - - -def email_pieces(voteinfo: TUVoteInfo) -> Tuple[str, str]: - """ - Return a (subject, content) tuple based on voteinfo.ID - - :param voteinfo: TUVoteInfo instance - :return: tuple(subject, content) - """ - subject = f"TU Vote Reminder: Proposal {voteinfo.ID}" - content = (f"Please remember to cast your vote on proposal {voteinfo.ID} " - f"[1]. The voting period\nends in less than 48 hours.\n\n" - f"[1] {aur_location}/tu/?id={voteinfo.ID}") - return (subject, content) - - -@pytest.fixture -def user(db_test) -> User: - yield create_user("test", TRUSTED_USER_ID) - - -@pytest.fixture -def user2() -> User: - yield create_user("test2", TRUSTED_USER_ID) - - -@pytest.fixture -def user3() -> User: - yield create_user("test3", TRUSTED_USER_ID) - - -@pytest.fixture -def voteinfo(user: User) -> TUVoteInfo: - now = time.utcnow() - start = config.getint("tuvotereminder", "range_start") - with db.begin(): - voteinfo = db.create(TUVoteInfo, Agenda="Lorem ipsum.", - User=user.Username, End=(now + start + 1), - Quorum=0.00, Submitter=user, Submitted=0) - yield voteinfo - - -def test_tu_vote_reminders(user: User, user2: User, user3: User, - voteinfo: TUVoteInfo): - reminder.main() - assert Email.count() == 3 - - emails = [Email(i).parse() for i in range(1, 4)] - subject, content = email_pieces(voteinfo) - expectations = [ - # (to, content) - (user.Email, subject, content), - (user2.Email, subject, content), - (user3.Email, subject, content) - ] - for i, element in enumerate(expectations): - email, subject, content = element - assert emails[i].headers.get("To") == email - assert emails[i].headers.get("Subject") == subject - assert emails[i].body == content - - -def test_tu_vote_reminders_only_unvoted(user: User, user2: User, user3: User, - voteinfo: TUVoteInfo): - # Vote with user2 and user3; leaving only user to be notified. - create_vote(user2, voteinfo) - create_vote(user3, voteinfo) - - reminder.main() - assert Email.count() == 1 - - email = Email(1).parse() - assert email.headers.get("To") == user.Email - - subject, content = email_pieces(voteinfo) - assert email.headers.get("Subject") == subject - assert email.body == content diff --git a/test/test_user.py b/test/test_user.py index 5f25f3c9..78a2a513 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -1,7 +1,6 @@ import hashlib import json - -from datetime import datetime, timedelta +from datetime import UTC, datetime, timedelta import bcrypt import pytest @@ -9,10 +8,14 @@ import pytest import aurweb.auth import aurweb.config import aurweb.models.account_type as at - from aurweb import db from aurweb.auth import creds -from aurweb.models.account_type import DEVELOPER_ID, TRUSTED_USER_AND_DEV_ID, TRUSTED_USER_ID, USER_ID +from aurweb.models.account_type import ( + DEVELOPER_ID, + PACKAGE_MAINTAINER_AND_DEV_ID, + PACKAGE_MAINTAINER_ID, + USER_ID, +) from aurweb.models.ban import Ban from aurweb.models.package import Package from aurweb.models.package_base import PackageBase @@ -31,10 +34,14 @@ def setup(db_test): def create_user(username: str, account_type_id: int): with db.begin(): - user = db.create(User, Username=username, - Email=f"{username}@example.org", - RealName=username.title(), Passwd="testPassword", - AccountTypeID=account_type_id) + user = db.create( + User, + Username=username, + Email=f"{username}@example.org", + RealName=username.title(), + Passwd="testPassword", + AccountTypeID=account_type_id, + ) return user @@ -45,8 +52,8 @@ def user() -> User: @pytest.fixture -def tu_user() -> User: - user = create_user("test_tu", TRUSTED_USER_ID) +def pm_user() -> User: + user = create_user("test_pm", PACKAGE_MAINTAINER_ID) yield user @@ -57,8 +64,8 @@ def dev_user() -> User: @pytest.fixture -def tu_and_dev_user() -> User: - user = create_user("test_tu_and_dev", TRUSTED_USER_AND_DEV_ID) +def pm_and_dev_user() -> User: + user = create_user("test_pm_and_dev", PACKAGE_MAINTAINER_AND_DEV_ID) yield user @@ -71,7 +78,7 @@ def package(user: User) -> Package: def test_user_login_logout(user: User): - """ Test creating a user and reading its columns. """ + """Test creating a user and reading its columns.""" # Assert that make_user created a valid user. assert bool(user.ID) @@ -89,8 +96,7 @@ def test_user_login_logout(user: User): assert user.is_authenticated() # Expect that User session relationships work right. - user_session = db.query(Session, - Session.UsersID == user.ID).first() + user_session = db.query(Session, Session.UsersID == user.ID).first() assert user_session == user.session assert user.session.SessionID == sid assert user.session.User == user @@ -111,8 +117,10 @@ def test_user_login_logout(user: User): assert result.is_authenticated() # Test out user string functions. - assert repr(user) == f"" + assert ( + repr(user) + == f"" + ) # Test logout. user.logout(request) @@ -127,7 +135,7 @@ def test_user_login_twice(user: User): def test_user_login_banned(user: User): # Add ban for the next 30 seconds. - banned_timestamp = datetime.utcnow() + timedelta(seconds=30) + banned_timestamp = datetime.now(UTC) + timedelta(seconds=30) with db.begin(): db.create(Ban, IPAddress="127.0.0.1", BanTS=banned_timestamp) @@ -145,9 +153,7 @@ def test_user_login_suspended(user: User): def test_legacy_user_authentication(user: User): with db.begin(): user.Salt = bcrypt.gensalt().decode() - user.Passwd = hashlib.md5( - f"{user.Salt}testPassword".encode() - ).hexdigest() + user.Passwd = hashlib.md5(f"{user.Salt}testPassword".encode()).hexdigest() assert not user.valid_password("badPassword") assert user.valid_password("testPassword") @@ -160,8 +166,12 @@ def test_user_login_with_outdated_sid(user: User): # Make a session with a LastUpdateTS 5 seconds ago, causing # user.login to update it with a new sid. with db.begin(): - db.create(Session, UsersID=user.ID, SessionID="stub", - LastUpdateTS=datetime.utcnow().timestamp() - 5) + db.create( + Session, + UsersID=user.ID, + SessionID="stub", + LastUpdateTS=datetime.now(UTC).timestamp() - 5, + ) sid = user.login(Request(), "testPassword") assert sid and user.is_authenticated() assert sid != "stub" @@ -186,41 +196,44 @@ def test_user_ssh_pub_key(user: User): assert user.ssh_pub_keys.first() is None with db.begin(): - ssh_pub_key = db.create(SSHPubKey, UserID=user.ID, - Fingerprint="testFingerprint", - PubKey="testPubKey") + ssh_pub_key = db.create( + SSHPubKey, + UserID=user.ID, + Fingerprint="testFingerprint", + PubKey="testPubKey", + ) assert user.ssh_pub_keys.first() == ssh_pub_key def test_user_credential_types(user: User): - assert user.AccountTypeID in creds.user_developer_or_trusted_user - assert user.AccountTypeID not in creds.trusted_user + assert user.AccountTypeID in creds.user_developer_or_package_maintainer + assert user.AccountTypeID not in creds.package_maintainer assert user.AccountTypeID not in creds.developer - assert user.AccountTypeID not in creds.trusted_user_or_dev + assert user.AccountTypeID not in creds.package_maintainer_or_dev with db.begin(): - user.AccountTypeID = at.TRUSTED_USER_ID + user.AccountTypeID = at.PACKAGE_MAINTAINER_ID - assert user.AccountTypeID in creds.trusted_user - assert user.AccountTypeID in creds.trusted_user_or_dev + assert user.AccountTypeID in creds.package_maintainer + assert user.AccountTypeID in creds.package_maintainer_or_dev with db.begin(): user.AccountTypeID = at.DEVELOPER_ID assert user.AccountTypeID in creds.developer - assert user.AccountTypeID in creds.trusted_user_or_dev + assert user.AccountTypeID in creds.package_maintainer_or_dev with db.begin(): - user.AccountTypeID = at.TRUSTED_USER_AND_DEV_ID + user.AccountTypeID = at.PACKAGE_MAINTAINER_AND_DEV_ID - assert user.AccountTypeID in creds.trusted_user + assert user.AccountTypeID in creds.package_maintainer assert user.AccountTypeID in creds.developer - assert user.AccountTypeID in creds.trusted_user_or_dev + assert user.AccountTypeID in creds.package_maintainer_or_dev # Some model authorization checks. assert user.is_elevated() - assert user.is_trusted_user() + assert user.is_package_maintainer() assert user.is_developer() @@ -242,15 +255,15 @@ def test_user_as_dict(user: User): assert isinstance(data.get("RegistrationTS"), datetime) -def test_user_is_trusted_user(user: User): +def test_user_is_package_maintainer(user: User): with db.begin(): - user.AccountTypeID = at.TRUSTED_USER_ID - assert user.is_trusted_user() is True + user.AccountTypeID = at.PACKAGE_MAINTAINER_ID + assert user.is_package_maintainer() is True # Do it again with the combined role. with db.begin(): - user.AccountTypeID = at.TRUSTED_USER_AND_DEV_ID - assert user.is_trusted_user() is True + user.AccountTypeID = at.PACKAGE_MAINTAINER_AND_DEV_ID + assert user.is_package_maintainer() is True def test_user_is_developer(user: User): @@ -260,13 +273,13 @@ def test_user_is_developer(user: User): # Do it again with the combined role. with db.begin(): - user.AccountTypeID = at.TRUSTED_USER_AND_DEV_ID + user.AccountTypeID = at.PACKAGE_MAINTAINER_AND_DEV_ID assert user.is_developer() is True def test_user_voted_for(user: User, package: Package): pkgbase = package.PackageBase - now = int(datetime.utcnow().timestamp()) + now = int(datetime.now(UTC).timestamp()) with db.begin(): db.create(PackageVote, PackageBase=pkgbase, User=user, VoteTS=now) assert user.voted_for(package) @@ -283,34 +296,35 @@ def test_user_packages(user: User, package: Package): assert package in user.packages() -def test_can_edit_user(user: User, tu_user: User, dev_user: User, - tu_and_dev_user: User): +def test_can_edit_user( + user: User, pm_user: User, dev_user: User, pm_and_dev_user: User +): # User can edit. assert user.can_edit_user(user) # User cannot edit. - assert not user.can_edit_user(tu_user) + assert not user.can_edit_user(pm_user) assert not user.can_edit_user(dev_user) - assert not user.can_edit_user(tu_and_dev_user) + assert not user.can_edit_user(pm_and_dev_user) - # Trusted User can edit. - assert tu_user.can_edit_user(user) - assert tu_user.can_edit_user(tu_user) + # Package Maintainer can edit. + assert pm_user.can_edit_user(user) + assert pm_user.can_edit_user(pm_user) - # Trusted User cannot edit. - assert not tu_user.can_edit_user(dev_user) - assert not tu_user.can_edit_user(tu_and_dev_user) + # Package Maintainer cannot edit. + assert not pm_user.can_edit_user(dev_user) + assert not pm_user.can_edit_user(pm_and_dev_user) # Developer can edit. assert dev_user.can_edit_user(user) - assert dev_user.can_edit_user(tu_user) + assert dev_user.can_edit_user(pm_user) assert dev_user.can_edit_user(dev_user) # Developer cannot edit. - assert not dev_user.can_edit_user(tu_and_dev_user) + assert not dev_user.can_edit_user(pm_and_dev_user) - # Trusted User & Developer can edit. - assert tu_and_dev_user.can_edit_user(user) - assert tu_and_dev_user.can_edit_user(tu_user) - assert tu_and_dev_user.can_edit_user(dev_user) - assert tu_and_dev_user.can_edit_user(tu_and_dev_user) + # Package Maintainer & Developer can edit. + assert pm_and_dev_user.can_edit_user(user) + assert pm_and_dev_user.can_edit_user(pm_user) + assert pm_and_dev_user.can_edit_user(dev_user) + assert pm_and_dev_user.can_edit_user(pm_and_dev_user) diff --git a/test/test_usermaint.py b/test/test_usermaint.py index e572569a..7d7bd135 100644 --- a/test/test_usermaint.py +++ b/test/test_usermaint.py @@ -14,13 +14,18 @@ def setup(db_test): @pytest.fixture def user() -> User: with db.begin(): - user = db.create(User, Username="test", Email="test@example.org", - Passwd="testPassword", AccountTypeID=USER_ID) + user = db.create( + User, + Username="test", + Email="test@example.org", + Passwd="testPassword", + AccountTypeID=USER_ID, + ) yield user def test_usermaint_noop(user: User): - """ Last[SSH]Login isn't expired in this test: usermaint is noop. """ + """Last[SSH]Login isn't expired in this test: usermaint is noop.""" now = time.utcnow() with db.begin(): diff --git a/test/test_util.py b/test/test_util.py index ae1de81b..1c3b51af 100644 --- a/test/test_util.py +++ b/test/test_util.py @@ -1,13 +1,12 @@ import json - from http import HTTPStatus import fastapi import pytest - from fastapi.responses import JSONResponse -from aurweb import filters, util +from aurweb import db, filters, util +from aurweb.models.user import User from aurweb.testing.requests import Request @@ -18,7 +17,7 @@ def test_round(): def test_git_search(): - """ Test that git_search matches the full commit if necessary. """ + """Test that git_search matches the full commit if necessary.""" commit_hash = "0123456789abcdef" repo = {commit_hash} prefixlen = util.git_search(repo, commit_hash) @@ -26,7 +25,7 @@ def test_git_search(): def test_git_search_double_commit(): - """ Test that git_search matches a shorter prefix length. """ + """Test that git_search matches a shorter prefix length.""" commit_hash = "0123456789abcdef" repo = {commit_hash[:13]} # Locate the shortest prefix length that matches commit_hash. @@ -36,7 +35,6 @@ def test_git_search_double_commit(): @pytest.mark.asyncio async def test_error_or_result(): - async def route(request: fastapi.Request): raise RuntimeError("No response returned.") @@ -47,7 +45,7 @@ async def test_error_or_result(): assert data.get("error") == "No response returned." async def good_route(request: fastapi.Request): - return JSONResponse() + return JSONResponse("{}") response = await util.error_or_result(good_route, Request()) assert response.status_code == HTTPStatus.OK @@ -99,14 +97,73 @@ YbxDwGimZZslg0OZu9UzoAT6xEGyiZsqJkTMbRp1ZYIOv9jHCJxRuxxuN3fzxyT3xE69+vhq2/NJX\ vTNJCD6JtMClxbIXW9q74nNqG+2SD/VQNMUz/505TK1PbY/4uyFfq5HquHJXQVCBll03FRerNHH2N\ schFne6BFHpa48PCoZNH45wLjFXwUyrGU1HrNqh6ZPdRfBTrTOkgs+BKBxGNeV45aYUPu/cFBSPcB\ fRSo6OFcejKc=""" + assert_multiple_keys(pks) + + +def test_parse_ssh_keys_with_extra_lines(): + pks = """ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyN\ +TYAAABBBEURnkiY6JoLyqDE8Li1XuAW+LHmkmLDMW/GL5wY7k4/A+Ta7bjA3MOKrF9j4EuUTvCuNX\ +ULxvpfSqheTFWZc+g= + + + + +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDmqEapFMh/ajPHnm1dBweYPeLOUjC0Ydp6uw7rB\ +S5KCggUVQR8WfIm+sRYTj2+smGsK6zHMBjFnbzvV11vnMqcnY+Sa4LhIAdwkbt/b8HlGaLj1hCWSh\ +a5b5/noeK7L+CECGHdvfJhpxBbhq38YEdFnCGbslk/4NriNcUp/DO81CXb1RzJ9GBFH8ivPW1mbe9\ +YbxDwGimZZslg0OZu9UzoAT6xEGyiZsqJkTMbRp1ZYIOv9jHCJxRuxxuN3fzxyT3xE69+vhq2/NJX\ +8aRsxGPL9G/XKcaYGS6y6LW4quIBCz/XsTZfx1GmkQeZPYHH8FeE+XC/+toXL/kamxdOQKFYEEpWK\ +vTNJCD6JtMClxbIXW9q74nNqG+2SD/VQNMUz/505TK1PbY/4uyFfq5HquHJXQVCBll03FRerNHH2N\ +schFne6BFHpa48PCoZNH45wLjFXwUyrGU1HrNqh6ZPdRfBTrTOkgs+BKBxGNeV45aYUPu/cFBSPcB\ +fRSo6OFcejKc= + + +""" + assert_multiple_keys(pks) + + +@pytest.mark.parametrize( + "offset_str, per_page_str, expected", + [ + ("5", "100", (5, 100)), + ("", "100", (0, 100)), + ("5", "", (5, 50)), + ("", "", (0, 50)), + ("-1", "100", (0, 100)), + ("5", "-100", (5, 50)), + ("0", "0", (0, 50)), + ], +) +def test_sanitize_params(offset_str: str, per_page_str: str, expected: tuple[int, int]): + assert util.sanitize_params(offset_str, per_page_str) == expected + + +def assert_multiple_keys(pks): keys = util.parse_ssh_keys(pks) assert len(keys) == 2 - pfx1, key1, pfx2, key2 = pks.split() - k1, k2 = keys + assert (pfx1, key1) in keys + assert (pfx2, key2) in keys - assert pfx1 == k1[0] - assert key1 == k1[1] - assert pfx2 == k2[0] - assert key2 == k2[1] +def test_hash_query(): + # No conditions + query = db.query(User) + assert util.hash_query(query) == "75e76026b7d576536e745ec22892cf8f5d7b5d62" + + # With where clause + query = db.query(User).filter(User.Username == "bla") + assert util.hash_query(query) == "4dca710f33b1344c27ec6a3c266970f4fa6a8a00" + + # With where clause and sorting + query = db.query(User).filter(User.Username == "bla").order_by(User.Username) + assert util.hash_query(query) == "ee2c7846fede430776e140f8dfe1d83cd21d2eed" + + # With where clause, sorting and specific columns + query = ( + db.query(User) + .filter(User.Username == "bla") + .order_by(User.Username) + .with_entities(User.Username) + ) + assert util.hash_query(query) == "c1db751be61443d266cf643005eee7a884dac103" diff --git a/test/test_voteinfo.py b/test/test_voteinfo.py new file mode 100644 index 00000000..99e14a8c --- /dev/null +++ b/test/test_voteinfo.py @@ -0,0 +1,177 @@ +import pytest +from sqlalchemy.exc import IntegrityError + +from aurweb import db, time +from aurweb.db import create, rollback +from aurweb.models.account_type import PACKAGE_MAINTAINER_ID +from aurweb.models.user import User +from aurweb.models.voteinfo import VoteInfo + + +@pytest.fixture(autouse=True) +def setup(db_test): + return + + +@pytest.fixture +def user() -> User: + with db.begin(): + user = create( + User, + Username="test", + Email="test@example.org", + RealName="Test User", + Passwd="testPassword", + AccountTypeID=PACKAGE_MAINTAINER_ID, + ) + yield user + + +def test_voteinfo_creation(user: User): + ts = time.utcnow() + with db.begin(): + voteinfo = create( + VoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=ts, + End=ts + 5, + Quorum=0.5, + Submitter=user, + ) + assert bool(voteinfo.ID) + assert voteinfo.Agenda == "Blah blah." + assert voteinfo.User == user.Username + assert voteinfo.Submitted == ts + assert voteinfo.End == ts + 5 + assert voteinfo.Quorum == 0.5 + assert voteinfo.Submitter == user + assert voteinfo.Yes == 0 + assert voteinfo.No == 0 + assert voteinfo.Abstain == 0 + assert voteinfo.ActiveUsers == 0 + + assert voteinfo in user.voteinfo_set + + +def test_voteinfo_is_running(user: User): + ts = time.utcnow() + with db.begin(): + voteinfo = create( + VoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=ts, + End=ts + 1000, + Quorum=0.5, + Submitter=user, + ) + assert voteinfo.is_running() is True + + with db.begin(): + voteinfo.End = ts - 5 + assert voteinfo.is_running() is False + + +def test_voteinfo_total_votes(user: User): + ts = time.utcnow() + with db.begin(): + voteinfo = create( + VoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=ts, + End=ts + 1000, + Quorum=0.5, + Submitter=user, + ) + + voteinfo.Yes = 1 + voteinfo.No = 3 + voteinfo.Abstain = 5 + + # total_votes() should be the sum of Yes, No and Abstain: 1 + 3 + 5 = 9. + assert voteinfo.total_votes() == 9 + + +def test_voteinfo_null_submitter_raises(user: User): + with pytest.raises(IntegrityError): + with db.begin(): + create( + VoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=0, + End=0, + Quorum=0.50, + ) + rollback() + + +def test_voteinfo_null_agenda_raises(user: User): + with pytest.raises(IntegrityError): + with db.begin(): + create( + VoteInfo, + User=user.Username, + Submitted=0, + End=0, + Quorum=0.50, + Submitter=user, + ) + rollback() + + +def test_voteinfo_null_user_raises(user: User): + with pytest.raises(IntegrityError): + with db.begin(): + create( + VoteInfo, + Agenda="Blah blah.", + Submitted=0, + End=0, + Quorum=0.50, + Submitter=user, + ) + rollback() + + +def test_voteinfo_null_submitted_raises(user: User): + with pytest.raises(IntegrityError): + with db.begin(): + create( + VoteInfo, + Agenda="Blah blah.", + User=user.Username, + End=0, + Quorum=0.50, + Submitter=user, + ) + rollback() + + +def test_voteinfo_null_end_raises(user: User): + with pytest.raises(IntegrityError): + with db.begin(): + create( + VoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=0, + Quorum=0.50, + Submitter=user, + ) + rollback() + + +def test_voteinfo_null_quorum_default(user: User): + with db.begin(): + vi = create( + VoteInfo, + Agenda="Blah blah.", + User=user.Username, + Submitted=0, + End=0, + Submitter=user, + ) + assert vi.Quorum == 0 diff --git a/test/test_votereminder.py b/test/test_votereminder.py new file mode 100644 index 00000000..39d20824 --- /dev/null +++ b/test/test_votereminder.py @@ -0,0 +1,114 @@ +from typing import Tuple + +import pytest + +from aurweb import config, db, time +from aurweb.models import User, Vote, VoteInfo +from aurweb.models.account_type import PACKAGE_MAINTAINER_ID +from aurweb.scripts import votereminder as reminder +from aurweb.testing.email import Email + +aur_location = config.get("options", "aur_location") + + +def create_vote(user: User, voteinfo: VoteInfo) -> Vote: + with db.begin(): + vote = db.create(Vote, User=user, VoteID=voteinfo.ID) + return vote + + +def create_user(username: str, type_id: int): + with db.begin(): + user = db.create( + User, + AccountTypeID=type_id, + Username=username, + Email=f"{username}@example.org", + Passwd=str(), + ) + return user + + +def email_pieces(voteinfo: VoteInfo) -> Tuple[str, str]: + """ + Return a (subject, content) tuple based on voteinfo.ID + + :param voteinfo: VoteInfo instance + :return: tuple(subject, content) + """ + subject = f"Package Maintainer Vote Reminder: Proposal {voteinfo.ID}" + content = ( + f"Please remember to cast your vote on proposal {voteinfo.ID} " + f"[1]. The voting period\nends in less than 48 hours.\n\n" + f"[1] {aur_location}/package-maintainer/?id={voteinfo.ID}" + ) + return subject, content + + +@pytest.fixture +def user(db_test) -> User: + yield create_user("test", PACKAGE_MAINTAINER_ID) + + +@pytest.fixture +def user2() -> User: + yield create_user("test2", PACKAGE_MAINTAINER_ID) + + +@pytest.fixture +def user3() -> User: + yield create_user("test3", PACKAGE_MAINTAINER_ID) + + +@pytest.fixture +def voteinfo(user: User) -> VoteInfo: + now = time.utcnow() + start = config.getint("votereminder", "range_start") + with db.begin(): + voteinfo = db.create( + VoteInfo, + Agenda="Lorem ipsum.", + User=user.Username, + End=(now + start + 1), + Quorum=0.00, + Submitter=user, + Submitted=0, + ) + yield voteinfo + + +def test_vote_reminders(user: User, user2: User, user3: User, voteinfo: VoteInfo): + reminder.main() + assert Email.count() == 3 + + emails = [Email(i).parse() for i in range(1, 4)] + subject, content = email_pieces(voteinfo) + expectations = [ + # (to, content) + (user.Email, subject, content), + (user2.Email, subject, content), + (user3.Email, subject, content), + ] + for i, element in enumerate(expectations): + email, subject, content = element + assert emails[i].headers.get("To") == email + assert emails[i].headers.get("Subject") == subject + assert emails[i].body == content + + +def test_vote_reminders_only_unvoted( + user: User, user2: User, user3: User, voteinfo: VoteInfo +): + # Vote with user2 and user3; leaving only user to be notified. + create_vote(user2, voteinfo) + create_vote(user3, voteinfo) + + reminder.main() + assert Email.count() == 1 + + email = Email(1).parse() + assert email.headers.get("To") == user.Email + + subject, content = email_pieces(voteinfo) + assert email.headers.get("Subject") == subject + assert email.body == content diff --git a/upgrading/3.4.0.txt b/upgrading/3.4.0.txt index c2f16888..b3804e94 100644 --- a/upgrading/3.4.0.txt +++ b/upgrading/3.4.0.txt @@ -1,5 +1,5 @@ -1. Add the "Trusted User & Developer" user group: +1. Add the "Package Maintainer & Developer" user group: ---- -INSERT INTO AccountTypes (ID, AccountType) VALUES (4, 'Trusted User & Developer'); +INSERT INTO AccountTypes (ID, AccountType) VALUES (4, 'Package Maintainer & Developer'); ---- diff --git a/util/fix-coverage b/util/fix-coverage index 3446c4af..77cf29c1 100755 --- a/util/fix-coverage +++ b/util/fix-coverage @@ -48,9 +48,8 @@ def main(): files[i] = path for _, i in enumerate(files.keys()): - new_path = re.sub(r'^/aurweb', aurwebdir, files[i]) - cursor.execute("UPDATE file SET path = ? WHERE id = ?", ( - new_path, i)) + new_path = re.sub(r"^/aurweb", aurwebdir, files[i]) + cursor.execute("UPDATE file SET path = ? WHERE id = ?", (new_path, i)) db.commit() db.close() diff --git a/web/html/404.php b/web/html/404.php deleted file mode 100644 index 9f81d115..00000000 --- a/web/html/404.php +++ /dev/null @@ -1,47 +0,0 @@ - - -
    -

    404 -

    -

    - -
      -
    • - : - -
    • -
    • - ' . htmlspecialchars($gitpkg) . '', - '' . htmlspecialchars($gitcmd) . '') ?> -
    • -
    • - ', '', - '' . htmlspecialchars($gitpkg) . '') ?> -
    • -
    - -
    - - - -
    -

    503 -

    -

    -
    - -\n"; -echo "

    ".__("Accounts")."

    \n"; - -if (isset($_COOKIE["AURSID"])) { - if ($action == "SearchAccounts") { - - # security check - # - if (has_credential(CRED_ACCOUNT_SEARCH)) { - # the user has entered search criteria, find any matching accounts - # - search_results_page(in_request("O"), in_request("SB"), - in_request("U"), in_request("T"), in_request("S"), - in_request("E"), in_request("R"), in_request("I"), - in_request("K")); - - } else { - # a non-privileged user is trying to access the search page - # - print __("You are not allowed to access this area.")."
    \n"; - } - - } elseif ($action == "DisplayAccount") { - # the user has clicked 'edit', display the account details in a form - # - if (empty($row)) { - print __("Could not retrieve information for the specified user."); - } else { - /* Verify user has permission to edit the account */ - if (can_edit_account($row)) { - display_account_form("UpdateAccount", - $row["Username"], - $row["AccountTypeID"], - $row["Suspended"], - $row["Email"], - $row["BackupEmail"], - $row["HideEmail"], - "", - "", - $row["RealName"], - $row["LangPreference"], - $row["Timezone"], - $row["Homepage"], - $row["IRCNick"], - $row["PGPKey"], - $PK, - $row["InactivityTS"] ? 1 : 0, - $row["CommentNotify"], - $row["UpdateNotify"], - $row["OwnershipNotify"], - $row["ID"], - $row["Username"]); - } else { - print __("You do not have permission to edit this account."); - } - } - - } elseif ($action == "DeleteAccount") { - /* Details for account being deleted. */ - if ($row && can_edit_account($row)) { - $uid_removal = $row['ID']; - $uid_session = uid_from_sid($_COOKIE['AURSID']); - $username = $row['Username']; - - if (in_request('confirm') && check_token()) { - if (check_passwd($uid_session, $_REQUEST['passwd']) == 1) { - user_delete($uid_removal); - header('Location: /'); - } else { - echo "
    • "; - echo __("Invalid password."); - echo "
    "; - include("account_delete.php"); - } - } else { - include("account_delete.php"); - } - } else { - print __("You do not have permission to edit this account."); - } - } elseif ($action == "AccountInfo") { - # no editing, just looking up user info - # - if (empty($row)) { - print __("Could not retrieve information for the specified user."); - } else { - include("account_details.php"); - } - - } elseif ($action == "UpdateAccount") { - print $update_account_message; - - if ($row && !$success) { - display_account_form("UpdateAccount", - in_request("U"), - in_request("T"), - in_request("S"), - in_request("E"), - in_request("BE"), - in_request("H"), - in_request("P"), - in_request("C"), - in_request("R"), - in_request("L"), - in_request("TZ"), - in_request("HP"), - in_request("I"), - in_request("K"), - in_request("PK"), - in_request("J"), - in_request("CN"), - in_request("UN"), - in_request("ON"), - in_request("ID"), - $row["Username"]); - } - - } elseif ($action == "ListComments") { - if ($row && has_credential(CRED_ACCOUNT_LIST_COMMENTS, array($row["ID"]))) { - # display the comment list if they're a TU/dev - - $total_comment_count = account_comments_count($row["ID"]); - list($pagination_templs, $per_page, $offset) = calculate_pagination($total_comment_count); - - $username = $row["Username"]; - $uid = $row["ID"]; - $comments = account_comments($uid, $per_page, $offset); - - $comment_section = "account"; - include('pkg_comments.php'); - - } else { - print __("You are not allowed to access this area."); - } - - } else { - if (has_credential(CRED_ACCOUNT_SEARCH)) { - # display the search page if they're a TU/dev - # - print __("Use this form to search existing accounts.")."
    \n"; - include('search_accounts_form.php'); - - } else { - print __("You are not allowed to access this area."); - } - } - -} else { - # visitor is not logged in - # - print __("You must log in to view user information."); -} - -echo ""; - -html_footer(AURWEB_VERSION); - -?> diff --git a/web/html/addvote.php b/web/html/addvote.php deleted file mode 100644 index 83b800b8..00000000 --- a/web/html/addvote.php +++ /dev/null @@ -1,117 +0,0 @@ -" . __("New proposal submitted.") . "

    \n"; - } else { -?> - - -

    - - -
    -

    - -
    -

    - - - -

    -

    - - -

    -

    -
    -
    - - - " /> -

    -
    -
    - - -
    -
    - -
    -

    -

    - 50, - 'SeB' => 'M', - 'K' => username_from_sid($_COOKIE["AURSID"]), - 'outdated' => 'on', - 'SB' => 'l', - 'SO' => 'a' - ); - pkg_search_page($params, false, $_COOKIE["AURSID"]); - ?> -

    - -
    -
    -

    -

    ">

    - 50, - 'SeB' => 'm', - 'K' => username_from_sid($_COOKIE["AURSID"]), - 'SB' => 'l', - 'SO' => 'd' - ); - pkg_search_page($params, false, $_COOKIE["AURSID"]); - ?> -
    -
    -

    -

    ">

    - 50, - 'SeB' => 'c', - 'K' => username_from_sid($_COOKIE["AURSID"]), - 'SB' => 'l', - 'SO' => 'd' - ); - pkg_search_page($params, false, $_COOKIE["AURSID"]); - ?> -
    - -
    -

    AUR

    -

    - ', - '', - '', - '' - ); - ?> - ', '', - '', - '' - ); - ?> - - -

    -

    - : - -

    -

    -
    -
    -

    -

    -
    -

    - ', - '' - ); - ?> -

    -
      -
    • :
    • -
    • :
    • -
    • :
    • -
    -

    - ', - '' - ); - ?> -

    -
    -

    -
    -

    - ', - '' - ); - ?> -

    - -

    - -

    -
      - $fingerprint): ?> -
    • :
    • - -
    - -
    -

    -
    -

    - ', - '', - '', - '' - ); - ?> -

    -
    -

    -
    -

    - ', - '', - '', - '' - ); - ?> -

    -
    -
    - -
    -
    -
    -
    -
    -
    - - - " maxlength="35" autocomplete="off"/> -
    -
    -
    -
    - -
    -
    - -
    - -
    - -
    - - -
    - - - '1'); - } - } - - include get_route('/' . $tokens[1]); -} elseif (!empty($tokens[1]) && '/' . $tokens[1] == get_pkgreq_route()) { - if (!empty($tokens[2])) { - /* TODO: Create a proper data structure to pass variables from - * the routing framework to the individual pages instead of - * initializing arbitrary variables here. */ - if (!empty($tokens[3]) && $tokens[3] == 'close') { - $pkgreq_id = $tokens[2]; - } else { - $pkgreq_id = null; - } - - if (!$pkgreq_id) { - header("HTTP/1.0 404 Not Found"); - include "./404.php"; - return; - } - } - - include get_route('/' . $tokens[1]); -} elseif (!empty($tokens[1]) && '/' . $tokens[1] == get_user_route()) { - if (!empty($tokens[2])) { - $_REQUEST['ID'] = uid_from_username($tokens[2]); - - if (!$_REQUEST['ID']) { - header("HTTP/1.0 404 Not Found"); - include "./404.php"; - return; - } - - if (!empty($tokens[3])) { - if ($tokens[3] == 'edit') { - $_REQUEST['Action'] = "DisplayAccount"; - } elseif ($tokens[3] == 'update') { - $_REQUEST['Action'] = "UpdateAccount"; - } elseif ($tokens[3] == 'delete') { - $_REQUEST['Action'] = "DeleteAccount"; - } elseif ($tokens[3] == 'comments') { - $_REQUEST['Action'] = "ListComments"; - } else { - header("HTTP/1.0 404 Not Found"); - include "./404.php"; - return; - } - } else { - $_REQUEST['Action'] = "AccountInfo"; - } - } - include get_route('/' . $tokens[1]); -} elseif (get_route($path) !== NULL) { - include get_route($path); -} else { - switch ($path) { - case "/css/archweb.css": - case "/css/aurweb.css": - case "/css/cgit.css": - case "/css/archnavbar/archnavbar.css": - header("Content-Type: text/css"); - readfile("./$path"); - break; - case "/images/ajax-loader.gif": - header("Content-Type: image/gif"); - readfile("./$path"); - break; - case "/css/archnavbar/archlogo.png": - case "/css/archnavbar/aurlogo.png": - case "/images/favicon.ico": - header("Content-Type: image/png"); - readfile("./$path"); - break; - case "/images/x.min.svg": - case "/images/action-undo.min.svg": - case "/images/pencil.min.svg": - case "/images/pin.min.svg": - case "/images/unpin.min.svg": - case "/images/rss.svg": - header("Content-Type: image/svg+xml"); - readfile("./$path"); - break; - case "/js/typeahead.js": - header("Content-Type: application/javascript"); - readfile("./$path"); - break; - case "/packages.gz": - case "/packages-meta-v1.json.gz": - case "/packages-meta-ext-v1.json.gz": - case "/pkgbase.gz": - case "/users.gz": - header("Content-Type: text/plain"); - header("Content-Encoding: gzip"); - readfile("./$path"); - break; - default: - header("HTTP/1.0 404 Not Found"); - include "./404.php"; - break; - } -} diff --git a/web/html/login.php b/web/html/login.php deleted file mode 100644 index 3f3d66cc..00000000 --- a/web/html/login.php +++ /dev/null @@ -1,68 +0,0 @@ - -
    -

    AUR

    - -

    - ' . username_from_sid($_COOKIE["AURSID"]) . ''); ?> - [] -

    - -
    -
    - - -
    - -

    - - -

    -

    - - -

    -

    - - -

    -

    - " /> - [] - - [] - - - - -

    -
    -
    - -

    - ', ''); ?> -

    - -
    -query("SELECT SSOAccountID FROM Users WHERE ID = " . $dbh->quote($uid)) - ->fetchColumn(); - if ($sso_account_id) - $redirect_uri = '/sso/logout'; - } -} - -header("Location: $redirect_uri"); - diff --git a/web/html/modified-rss.php b/web/html/modified-rss.php deleted file mode 100644 index 4c5c47e0..00000000 --- a/web/html/modified-rss.php +++ /dev/null @@ -1,62 +0,0 @@ -cssStyleSheet = false; -$rss->xslStyleSheet = false; - -# Use UTF-8 (fixes FS#10706). -$rss->encoding = "UTF-8"; - -#All the general RSS setup -$rss->title = "AUR Latest Modified Packages"; -$rss->description = "The latest modified packages in the AUR"; -$rss->link = "${protocol}://{$host}"; -$rss->syndicationURL = "{$protocol}://{$host}" . get_uri('/rss/'); -$image = new FeedImage(); -$image->title = "AUR Latest Modified Packages"; -$image->url = "{$protocol}://{$host}/css/archnavbar/aurlogo.png"; -$image->link = $rss->link; -$image->description = "AUR Latest Modified Packages Feed"; -$rss->image = $image; - -#Get the latest packages and add items for them -$packages = latest_modified_pkgs(100); - -foreach ($packages as $indx => $row) { - $item = new FeedItem(); - $item->title = $row["Name"]; - $item->link = "{$protocol}://{$host}" . get_pkg_uri($row["Name"]); - $item->description = $row["Description"]; - $item->date = intval($row["ModifiedTS"]); - $item->source = "{$protocol}://{$host}"; - $item->author = username_from_id($row["MaintainerUID"]); - $item->guidIsPermaLink = true; - $item->guid = $row["Name"] . "-" . $row["ModifiedTS"]; - $rss->addItem($item); -} - -#save it so that useCached() can find it -$feedContent = $rss->createFeed(); -set_cache_value($feed_key, $feedContent, 600); -echo $feedContent; -?> diff --git a/web/html/packages.php b/web/html/packages.php deleted file mode 100644 index d19d2ad1..00000000 --- a/web/html/packages.php +++ /dev/null @@ -1,174 +0,0 @@ - - - - -\n"; - } -} else { - if (!isset($_GET['K']) && !isset($_GET['SB'])) { - $_GET['SB'] = 'p'; - $_GET['SO'] = 'd'; - } - echo '
    '; - if (isset($_COOKIE["AURSID"])) { - pkg_search_page($_GET, true, $_COOKIE["AURSID"]); - } else { - pkg_search_page($_GET, true); - } - echo '
    '; -} - -html_footer(AURWEB_VERSION); - diff --git a/web/html/passreset.php b/web/html/passreset.php deleted file mode 100644 index 26b9bbbb..00000000 --- a/web/html/passreset.php +++ /dev/null @@ -1,100 +0,0 @@ - - -
    -

    - - -

    - -

    - - -
    - -
    - - - - - - - - - - - - - -
    -
    - -
    - -

    ', - ''); ?>

    - -
    - -
    -

    -

    - -
    - -
    - - $i) { - $id = intval($id); - if ($id > 0) { - $ids[] = $id; - } - } -} - -/* Perform package base actions. */ -$via = isset($_POST['via']) ? $_POST['via'] : NULL; -$return_to = isset($_POST['return_to']) ? $_POST['return_to'] : NULL; -$ret = false; -$output = ""; -$fragment = ""; -if (check_token()) { - if (current_action("do_Flag")) { - list($ret, $output) = pkgbase_flag($ids, $_POST['comments']); - } elseif (current_action("do_UnFlag")) { - list($ret, $output) = pkgbase_unflag($ids); - } elseif (current_action("do_Adopt")) { - list($ret, $output) = pkgbase_adopt($ids, true, NULL); - } elseif (current_action("do_Disown")) { - if (isset($_POST['confirm'])) { - list($ret, $output) = pkgbase_adopt($ids, false, $via); - } else { - $output = __("The selected packages have not been disowned, check the confirmation checkbox."); - $ret = false; - } - } elseif (current_action("do_DisownComaintainer")) { - $uid = uid_from_sid($_COOKIE["AURSID"]); - list($ret, $output) = pkgbase_remove_comaintainer($base_id, $uid); - } elseif (current_action("do_Vote")) { - list($ret, $output) = pkgbase_vote($ids, true); - } elseif (current_action("do_UnVote")) { - list($ret, $output) = pkgbase_vote($ids, false); - } elseif (current_action("do_Delete")) { - if (isset($_POST['confirm'])) { - if (!isset($_POST['merge_Into']) || empty($_POST['merge_Into'])) { - list($ret, $output) = pkgbase_delete($ids, NULL, $via); - unset($_GET['ID']); - unset($base_id); - } - else { - $merge_base_id = pkgbase_from_name($_POST['merge_Into']); - if (!$merge_base_id) { - $output = __("Cannot find package to merge votes and comments into."); - $ret = false; - } elseif (in_array($merge_base_id, $ids)) { - $output = __("Cannot merge a package base with itself."); - $ret = false; - } else { - list($ret, $output) = pkgbase_delete($ids, $merge_base_id, $via); - unset($_GET['ID']); - unset($base_id); - } - } - } - else { - $output = __("The selected packages have not been deleted, check the confirmation checkbox."); - $ret = false; - } - } elseif (current_action("do_Notify")) { - list($ret, $output) = pkgbase_notify($ids); - } elseif (current_action("do_UnNotify")) { - list($ret, $output) = pkgbase_notify($ids, false); - } elseif (current_action("do_DeleteComment")) { - list($ret, $output) = pkgbase_delete_comment(); - } elseif (current_action("do_UndeleteComment")) { - list($ret, $output) = pkgbase_delete_comment(true); - if ($ret && isset($_POST["comment_id"])) { - $fragment = '#comment-' . intval($_POST["comment_id"]); - } - } elseif (current_action("do_PinComment")) { - list($ret, $output) = pkgbase_pin_comment(); - } elseif (current_action("do_UnpinComment")) { - list($ret, $output) = pkgbase_pin_comment(true); - } elseif (current_action("do_SetKeywords")) { - list($ret, $output) = pkgbase_set_keywords($base_id, preg_split("/[\s,;]+/", $_POST['keywords'], -1, PREG_SPLIT_NO_EMPTY)); - } elseif (current_action("do_FileRequest")) { - list($ret, $output) = pkgreq_file($ids, $_POST['type'], $_POST['merge_into'], $_POST['comments']); - } elseif (current_action("do_CloseRequest")) { - list($ret, $output) = pkgreq_close($_POST['reqid'], $_POST['reason'], $_POST['comments']); - } elseif (current_action("do_EditComaintainers")) { - list($ret, $output) = pkgbase_set_comaintainers($base_id, explode("\n", $_POST['users'])); - } elseif (current_action("do_AddComment")) { - $uid = uid_from_sid($_COOKIE["AURSID"]); - list($ret, $output) = pkgbase_add_comment($base_id, $uid, $_REQUEST['comment']); - if ($ret && isset($_REQUEST['enable_notifications'])) { - list($ret, $output) = pkgbase_notify(array($base_id)); - } - $fragment = '#news'; - } elseif (current_action("do_EditComment")) { - list($ret, $output) = pkgbase_edit_comment($_REQUEST['comment']); - if ($ret && isset($_POST["comment_id"])) { - $fragment = '#comment-' . intval($_POST["comment_id"]); - } - } - - if ($ret) { - if (current_action("do_CloseRequest") || - (current_action("do_Delete") && $via)) { - /* Redirect back to package request page on success. */ - header('Location: ' . get_pkgreq_route()); - exit(); - } elseif ((current_action("do_DeleteComment") || - current_action("do_UndeleteComment")) && $return_to) { - header('Location: ' . $return_to); - exit(); - } elseif (current_action("do_PinComment") && $return_to) { - header('Location: ' . $return_to); - exit(); - } elseif (isset($base_id)) { - /* Redirect back to package base page on success. */ - header('Location: ' . get_pkgbase_uri($pkgbase_name) . $fragment); - exit(); - } else { - /* Redirect back to package search page. */ - header('Location: ' . get_pkg_route()); - exit(); - } - } -} - -if (isset($base_id)) { - $pkgs = pkgbase_get_pkgnames($base_id); - if (!$output && count($pkgs) == 1) { - /* Not a split package. Redirect to the package page. */ - if (empty($_SERVER['QUERY_STRING'])) { - header('Location: ' . get_pkg_uri($pkgs[0]) . $fragment); - } else { - header('Location: ' . get_pkg_uri($pkgs[0]) . '?' . $_SERVER['QUERY_STRING'] . $fragment); - } - } - - $details = pkgbase_get_details($base_id); -} else { - $details = array(); -} -html_header($title, $details); -?> - - - -

    - -
    - - - - -
    -

    :

    -

    - ', htmlspecialchars($pkgbase_name), ''); ?> -

    -
      - -
    • - -
    -

    - - -

    -
    -
    - - - - - - -

    -

    " />

    -
    -
    -
    - - -
    -

    :

    -

    - ', htmlspecialchars($pkgbase_name), ''); ?> -

    -
      - -
    • - -
    -

    - - - - 0 && !has_credential(CRED_PKGBASE_DISOWN)): ?> - ', $comaintainers[0], ''); ?> - - - -

    -
    -
    - - - - - - -

    " />

    -
    -
    -
    - - $i) { - $id = intval($id); - if ($id > 0) { - $ids[] = $id; - } - } -} - -/* Perform package base actions. */ -$ret = false; -$output = ""; -if (check_token()) { - if (current_action("do_Flag")) { - list($ret, $output) = pkgbase_flag($ids, $_POST['comments']); - } - - if ($ret) { - header('Location: ' . get_pkgbase_uri($pkgbase_name)); - exit(); - } -} - -/* Get default comment. */ -$comment = ''; -if (isset($_POST['comments'])) { - $comment = $_POST['comments']; -} - -html_header(__("Flag Package Out-Of-Date")); - -if (has_credential(CRED_PKGBASE_FLAG)): ?> -
    -

    :

    -

    - ', htmlspecialchars($pkgbase_name), ''); ?> -

    -
      - -
    • - -
    - -

    - This seems to be a VCS package. Please do not - flag it out-of-date if the package version in the AUR does not - match the most recent commit. Flagging this package should only - be done if the sources moved or changes in the PKGBUILD are - required because of recent upstream changes. -

    - -

    - ', ''); ?> - -

    - - -
    - - -
    -
    - - - -

    - - -

    -

    " />

    -
    -
    -
    - - -
    -

    :

    -

    - ', htmlspecialchars($pkgbase_name), ''); ?> - -

    -
      - -
    • - -
    -

    - - - -

    -
    -
    - - - - - - - - -

    -

    -

    -

    " />

    -
    -
    -
    - - 0) ? $_GET['PP'] : 50; - $current = ceil($first / $per_page); - $pages = ceil($total / $per_page); - $templ_pages = array(); - - if ($current > 1) { - $templ_pages['« ' . __('First')] = 0; - $templ_pages['‹ ' . __('Previous')] = ($current - 2) * $per_page; - } - - if ($current - 5 > 1) - $templ_pages["..."] = false; - - for ($i = max($current - 5, 1); $i <= min($pages, $current + 5); $i++) { - $templ_pages[$i] = ($i - 1) * $per_page; - } - - if ($current + 5 < $pages) - $templ_pages["... "] = false; - - if ($current < $pages) { - $templ_pages[__('Next') . ' ›'] = $current * $per_page; - $templ_pages[__('Last') . ' »'] = ($pages - 1) * $per_page; - } - - $SID = $_COOKIE['AURSID']; - - html_header(__("Requests")); - echo '
    '; - $show_headers = true; - include('pkgreq_results.php'); - echo '
    '; -} - -html_footer(AURWEB_VERSION); - diff --git a/web/html/register.php b/web/html/register.php deleted file mode 100644 index fee0a68f..00000000 --- a/web/html/register.php +++ /dev/null @@ -1,87 +0,0 @@ -'; -echo '

    ' . __('Register') . '

    '; - -if (in_request("Action") == "NewAccount") { - list($success, $message) = process_account_form( - "new", - "NewAccount", - in_request("U"), - 1, - 0, - in_request("E"), - in_request("BE"), - in_request("H"), - '', - '', - in_request("R"), - in_request("L"), - in_request("TZ"), - in_request("HP"), - in_request("I"), - in_request("K"), - in_request("PK"), - 0, - in_request("CN"), - in_request("UN"), - in_request("ON"), - 0, - "", - '', - in_request("captcha_salt"), - in_request("captcha"), - ); - - print $message; - - if (!$success) { - display_account_form("NewAccount", - in_request("U"), - 1, - 0, - in_request("E"), - in_request("BE"), - in_request("H"), - '', - '', - in_request("R"), - in_request("L"), - in_request("TZ"), - in_request("HP"), - in_request("I"), - in_request("K"), - in_request("PK"), - 0, - in_request("CN"), - in_request("UN"), - in_request("ON"), - 0, - "", - '', - in_request("captcha_salt"), - in_request("captcha") - ); - } -} else { - print '

    ' . __("Use this form to create an account.") . '

    '; - display_account_form("NewAccount", "", "", "", "", "", "", "", "", "", $LANG); -} - -echo ''; - -html_footer(AURWEB_VERSION); - -?> diff --git a/web/html/rpc.php b/web/html/rpc.php deleted file mode 100644 index 64c95622..00000000 --- a/web/html/rpc.php +++ /dev/null @@ -1,17 +0,0 @@ -handle($_GET); -} -else { - echo file_get_contents('../../doc/rpc.html'); -} -?> diff --git a/web/html/rss.php b/web/html/rss.php deleted file mode 100644 index 1e6335cf..00000000 --- a/web/html/rss.php +++ /dev/null @@ -1,61 +0,0 @@ -cssStyleSheet = false; -$rss->xslStyleSheet = false; - -# Use UTF-8 (fixes FS#10706). -$rss->encoding = "UTF-8"; - -#All the general RSS setup -$rss->title = "AUR Newest Packages"; -$rss->description = "The latest and greatest packages in the AUR"; -$rss->link = "${protocol}://{$host}"; -$rss->syndicationURL = "{$protocol}://{$host}" . get_uri('/rss/'); -$image = new FeedImage(); -$image->title = "AUR Newest Packages"; -$image->url = "{$protocol}://{$host}/css/archnavbar/aurlogo.png"; -$image->link = $rss->link; -$image->description = "AUR Newest Packages Feed"; -$rss->image = $image; - -#Get the latest packages and add items for them -$packages = latest_pkgs(100); - -foreach ($packages as $indx => $row) { - $item = new FeedItem(); - $item->title = $row["Name"]; - $item->link = "{$protocol}://{$host}" . get_pkg_uri($row["Name"]); - $item->description = $row["Description"]; - $item->date = intval($row["SubmittedTS"]); - $item->source = "{$protocol}://{$host}"; - $item->author = username_from_id($row["MaintainerUID"]); - $item->guid = $item->link; - $rss->addItem($item); -} - -#save it so that useCached() can find it -$feedContent = $rss->createFeed(); -set_cache_value($feed_key, $feedContent, 600); -echo $feedContent; -?> diff --git a/web/html/tos.php b/web/html/tos.php deleted file mode 100644 index fc5d8765..00000000 --- a/web/html/tos.php +++ /dev/null @@ -1,50 +0,0 @@ - -
    -

    AUR

    - -
    -
    -

    - ' . username_from_sid($_COOKIE["AURSID"]) . ''); ?> -

    -

    - -

    -
      - -
    • "> ()
    • - -
    -

    - - ]" value="" /> - - -

    -

    - " /> -

    -
    -
    - -
    - time() ? 1 : 0; - - # List voters of a proposal. - $whovoted = voter_list($row['ID']); - - $canvote = 1; - $hasvoted = 0; - $errorvote = ""; - if ($isrunning == 0) { - $canvote = 0; - $errorvote = __("Voting is closed for this proposal."); - } else if (!has_credential(CRED_TU_VOTE)) { - $canvote = 0; - $errorvote = __("Only Trusted Users are allowed to vote."); - } else if ($row['User'] == username_from_sid($_COOKIE["AURSID"])) { - $canvote = 0; - $errorvote = __("You cannot vote in an proposal about you."); - } - if (tu_voted($row['ID'], uid_from_sid($_COOKIE["AURSID"]))) { - $canvote = 0; - $hasvoted = 1; - if ($isrunning) { - $errorvote = __("You've already voted for this proposal."); - } - } - - if ($canvote == 1) { - if (isset($_POST['doVote']) && check_token()) { - if (isset($_POST['voteYes'])) { - $myvote = "Yes"; - } else if (isset($_POST['voteNo'])) { - $myvote = "No"; - } else if (isset($_POST['voteAbstain'])) { - $myvote = "Abstain"; - } - - cast_proposal_vote($row['ID'], uid_from_sid($_COOKIE["AURSID"]), $myvote, $row[$myvote] + 1); - - # Can't vote anymore - # - $canvote = 0; - $errorvote = __("You've already voted for this proposal."); - - # Update if they voted - if (tu_voted($row['ID'], uid_from_sid($_COOKIE["AURSID"]))) { - $hasvoted = 1; - } - $row = vote_details($_GET['id']); - } - } - include("tu_details.php"); - } - } else { - print __("Vote ID not valid."); - } - - } else { - $limit = $pp; - if (isset($_GET['off'])) - $offset = $_GET['off']; - - if (isset($_GET['by'])) - $by = $_GET['by']; - else - $by = 'desc'; - - if (!empty($offset) && is_numeric($offset)) { - if ($offset >= 1) { - $off = $offset; - } else { - $off = 0; - } - } else { - $off = 0; - } - - $order = ($by == 'asc') ? 'ASC' : 'DESC'; - $lim = ($limit > 0) ? " LIMIT $limit OFFSET $off" : ""; - $by_next = ($by == 'desc') ? 'asc' : 'desc'; - - $result = current_proposal_list($order); - $type = __("Current Votes"); - $nextresult = 0; - include("tu_list.php"); - - $result = past_proposal_list($order, $lim); - $type = __("Past Votes"); - $nextresult = proposal_count(); - include("tu_list.php"); - - $result = last_votes_list(); - include("tu_last_votes_list.php"); - } -} -else { - header('Location: /'); -} - -html_footer(AURWEB_VERSION); - diff --git a/web/html/voters.php b/web/html/voters.php deleted file mode 100644 index bacbcfc8..00000000 --- a/web/html/voters.php +++ /dev/null @@ -1,34 +0,0 @@ - - -
    -

    Votes for

    -
    -
      - $row): ?> -
    • - - 0): ?> - () - -
    • - -
    -
    -
    - -exec("SET NAMES 'utf8' COLLATE 'utf8_general_ci';"); - } else if ($backend == "sqlite") { - $dsn = $backend . - ":" . $name; - - self::$dbh = new PDO($dsn, null, null); - } else { - die("Error - " . $backend . " is not supported by aurweb"); - } - - } catch (PDOException $e) { - die('Error - Could not connect to AUR database'); - } - } - - return self::$dbh; - } -} diff --git a/web/lib/acctfuncs.inc.php b/web/lib/acctfuncs.inc.php deleted file mode 100644 index 0d021f99..00000000 --- a/web/lib/acctfuncs.inc.php +++ /dev/null @@ -1,1522 +0,0 @@ -\n" - . "
  • " . __("It must be between %s and %s characters long", $length_min, $length_max) - . "
  • " - . "
  • " . __("Start and end with a letter or number") . "
  • " - . "
  • " . __("Can contain only one period, underscore or hyphen.") - . "
  • \n"; - } - - if (!$error && $P && !$C) { - $error = __("Please confirm your new password."); - } - if (!$error && $P && $P != $C) { - $error = __("Password fields do not match."); - } - if (!$error && $P != '' && !good_passwd($P)) { - $length_min = config_get_int('options', 'passwd_min_len'); - $error = __("Your password must be at least %s characters.", - $length_min); - } - - if (!$error && !valid_email($E)) { - $error = __("The email address is invalid."); - } - if (!$error && $BE && !valid_email($BE)) { - $error = __("The backup email address is invalid."); - } - - if (!$error && !empty($HP) && !valid_homepage($HP)) { - $error = __("The home page is invalid, please specify the full HTTP(s) URL."); - } - - if (!$error && $K != '' && !valid_pgp_fingerprint($K)) { - $error = __("The PGP key fingerprint is invalid."); - } - - if (!$error && !empty($PK)) { - $ssh_keys = array_filter(array_map('trim', explode("\n", $PK))); - $ssh_fingerprints = array(); - - foreach ($ssh_keys as &$ssh_key) { - if (!valid_ssh_pubkey($ssh_key)) { - $error = __("The SSH public key is invalid."); - break; - } - - $ssh_fingerprint = ssh_key_fingerprint($ssh_key); - if (!$ssh_fingerprint) { - $error = __("The SSH public key is invalid."); - break; - } - - $tokens = explode(" ", $ssh_key); - $ssh_key = $tokens[0] . " " . $tokens[1]; - - $ssh_fingerprints[] = $ssh_fingerprint; - } - - /* - * Destroy last reference to prevent accidentally overwriting - * an array element. - */ - unset($ssh_key); - } - - if (isset($_COOKIE['AURSID'])) { - $atype = account_from_sid($_COOKIE['AURSID']); - if (($atype == "User" && $T > 1) || ($atype == "Trusted User" && $T > 2)) { - $error = __("Cannot increase account permissions."); - } - } - - if (!$error && !array_key_exists($L, $SUPPORTED_LANGS)) { - $error = __("Language is not currently supported."); - } - if (!$error && !array_key_exists($TZ, generate_timezone_list())) { - $error = __("Timezone is not currently supported."); - } - if (!$error) { - /* - * Check whether the user name is available. - * TODO: Fix race condition. - */ - $q = "SELECT COUNT(*) AS CNT FROM Users "; - $q.= "WHERE Username = " . $dbh->quote($U); - if ($TYPE == "edit") { - $q.= " AND ID != ".intval($UID); - } - $result = $dbh->query($q); - $row = $result->fetch(PDO::FETCH_NUM); - - if ($row[0]) { - $error = __("The username, %s%s%s, is already in use.", - "", htmlspecialchars($U,ENT_QUOTES), ""); - } - } - if (!$error) { - /* - * Check whether the e-mail address is available. - * TODO: Fix race condition. - */ - $q = "SELECT COUNT(*) AS CNT FROM Users "; - $q.= "WHERE Email = " . $dbh->quote($E); - if ($TYPE == "edit") { - $q.= " AND ID != ".intval($UID); - } - $result = $dbh->query($q); - $row = $result->fetch(PDO::FETCH_NUM); - - if ($row[0]) { - $error = __("The address, %s%s%s, is already in use.", - "", htmlspecialchars($E,ENT_QUOTES), ""); - } - } - if (!$error && isset($ssh_keys) && count($ssh_keys) > 0) { - /* - * Check whether any of the SSH public keys is already in use. - * TODO: Fix race condition. - */ - $q = "SELECT Fingerprint FROM SSHPubKeys "; - $q.= "WHERE Fingerprint IN ("; - $q.= implode(',', array_map(array($dbh, 'quote'), $ssh_fingerprints)); - $q.= ")"; - if ($TYPE == "edit") { - $q.= " AND UserID != " . intval($UID); - } - $result = $dbh->query($q); - $row = $result->fetch(PDO::FETCH_NUM); - - if ($row) { - $error = __("The SSH public key, %s%s%s, is already in use.", - "", htmlspecialchars($row[0], ENT_QUOTES), ""); - } - } - - if (!$error && $TYPE == "new" && empty($captcha)) { - $error = __("The CAPTCHA is missing."); - } - - if (!$error && $TYPE == "new" && !in_array($captcha_salt, get_captcha_salts())) { - $error = __("This CAPTCHA has expired. Please try again."); - } - - if (!$error && $TYPE == "new" && $captcha != get_captcha_answer($captcha_salt)) { - $error = __("The entered CAPTCHA answer is invalid."); - } - - if ($error) { - $message = "
    • ".$error."
    \n"; - return array(false, $message); - } - - if ($TYPE == "new") { - /* Create an unprivileged user. */ - if (empty($P)) { - $send_resetkey = true; - $email = $E; - } else { - $send_resetkey = false; - $P = password_hash($P, PASSWORD_DEFAULT); - } - $U = $dbh->quote($U); - $E = $dbh->quote($E); - $BE = $dbh->quote($BE); - $P = $dbh->quote($P); - $R = $dbh->quote($R); - $L = $dbh->quote($L); - $TZ = $dbh->quote($TZ); - $HP = $dbh->quote($HP); - $I = $dbh->quote($I); - $K = $dbh->quote(str_replace(" ", "", $K)); - $q = "INSERT INTO Users (AccountTypeID, Suspended, "; - $q.= "InactivityTS, Username, Email, BackupEmail, Passwd , "; - $q.= "RealName, LangPreference, Timezone, Homepage, IRCNick, PGPKey) "; - $q.= "VALUES (1, 0, 0, $U, $E, $BE, $P, $R, $L, $TZ, "; - $q.= "$HP, $I, $K)"; - $result = $dbh->exec($q); - if (!$result) { - $message = __("Error trying to create account, %s%s%s.", - "", htmlspecialchars($U,ENT_QUOTES), ""); - return array(false, $message); - } - - $uid = $dbh->lastInsertId(); - if (isset($ssh_keys) && count($ssh_keys) > 0) { - account_set_ssh_keys($uid, $ssh_keys, $ssh_fingerprints); - } - - $message = __("The account, %s%s%s, has been successfully created.", - "", htmlspecialchars($U,ENT_QUOTES), ""); - $message .= "

    \n"; - - if ($send_resetkey) { - send_resetkey($email, true); - $message .= __("A password reset key has been sent to your e-mail address."); - $message .= "

    \n"; - } else { - $message .= __("Click on the Login link above to use your account."); - $message .= "

    \n"; - } - } else { - /* Modify an existing account. */ - $q = "SELECT InactivityTS FROM Users WHERE "; - $q.= "ID = " . intval($UID); - $result = $dbh->query($q); - $row = $result->fetch(PDO::FETCH_NUM); - if ($row[0] && $J) { - $inactivity_ts = $row[0]; - } elseif ($J) { - $inactivity_ts = time(); - } else { - $inactivity_ts = 0; - } - - $q = "UPDATE Users SET "; - $q.= "Username = " . $dbh->quote($U); - if ($T) { - $q.= ", AccountTypeID = ".intval($T); - } - if ($S) { - /* Ensure suspended users can't keep an active session */ - delete_user_sessions($UID); - $q.= ", Suspended = 1"; - } else { - $q.= ", Suspended = 0"; - } - $q.= ", Email = " . $dbh->quote($E); - $q.= ", BackupEmail = " . $dbh->quote($BE); - if ($H) { - $q.= ", HideEmail = 1"; - } else { - $q.= ", HideEmail = 0"; - } - if ($P) { - $hash = password_hash($P, PASSWORD_DEFAULT); - $q .= ", Passwd = " . $dbh->quote($hash); - } - $q.= ", RealName = " . $dbh->quote($R); - $q.= ", LangPreference = " . $dbh->quote($L); - $q.= ", Timezone = " . $dbh->quote($TZ); - $q.= ", Homepage = " . $dbh->quote($HP); - $q.= ", IRCNick = " . $dbh->quote($I); - $q.= ", PGPKey = " . $dbh->quote(str_replace(" ", "", $K)); - $q.= ", InactivityTS = " . $inactivity_ts; - $q.= ", CommentNotify = " . ($CN ? "1" : "0"); - $q.= ", UpdateNotify = " . ($UN ? "1" : "0"); - $q.= ", OwnershipNotify = " . ($ON ? "1" : "0"); - $q.= " WHERE ID = ".intval($UID); - $result = $dbh->exec($q); - - if (isset($ssh_keys) && count($ssh_keys) > 0) { - $ssh_key_result = account_set_ssh_keys($UID, $ssh_keys, $ssh_fingerprints); - } else { - $ssh_key_result = true; - } - - if (isset($_COOKIE["AURTZ"]) && ($_COOKIE["AURTZ"] != $TZ)) { - /* set new cookie for timezone */ - $timeout = intval(config_get("options", "persistent_cookie_timeout")); - $cookie_time = time() + $timeout; - setcookie("AURTZ", $TZ, $cookie_time, "/"); - } - - if (isset($_COOKIE["AURLANG"]) && ($_COOKIE["AURLANG"] != $L)) { - /* set new cookie for language */ - $timeout = intval(config_get("options", "persistent_cookie_timeout")); - $cookie_time = time() + $timeout; - setcookie("AURLANG", $L, $cookie_time, "/"); - } - - if ($result === false || $ssh_key_result === false) { - $message = __("No changes were made to the account, %s%s%s.", - "", htmlspecialchars($U,ENT_QUOTES), ""); - } else { - $message = __("The account, %s%s%s, has been successfully modified.", - "", htmlspecialchars($U,ENT_QUOTES), ""); - } - } - - return array(true, $message); -} - -/** - * Display the search results page - * - * @param string $O The offset for the results page - * @param string $SB The column to sort the results page by - * @param string $U The username search criteria - * @param string $T The account type search criteria - * @param string $S Whether the account is suspended search criteria - * @param string $E The e-mail address search criteria - * @param string $R The real name search criteria - * @param string $I The IRC nickname search criteria - * @param string $K The PGP key fingerprint search criteria - * - * @return void - */ -function search_results_page($O=0,$SB="",$U="",$T="", - $S="",$E="",$R="",$I="",$K="") { - - $HITS_PER_PAGE = 50; - if ($O) { - $OFFSET = intval($O); - } else { - $OFFSET = 0; - } - if ($OFFSET < 0) { - $OFFSET = 0; - } - $search_vars = array(); - - $dbh = DB::connect(); - - $q = "SELECT Users.*, AccountTypes.AccountType "; - $q.= "FROM Users, AccountTypes "; - $q.= "WHERE AccountTypes.ID = Users.AccountTypeID "; - if ($T == "u") { - $q.= "AND AccountTypes.ID = 1 "; - $search_vars[] = "T"; - } elseif ($T == "t") { - $q.= "AND AccountTypes.ID = 2 "; - $search_vars[] = "T"; - } elseif ($T == "d") { - $q.= "AND AccountTypes.ID = 3 "; - $search_vars[] = "T"; - } elseif ($T == "td") { - $q.= "AND AccountTypes.ID = 4 "; - $search_vars[] = "T"; - } - if ($S) { - $q.= "AND Users.Suspended = 1 "; - $search_vars[] = "S"; - } - if ($U) { - $U = "%" . addcslashes($U, '%_') . "%"; - $q.= "AND Username LIKE " . $dbh->quote($U) . " "; - $search_vars[] = "U"; - } - if ($E) { - $E = "%" . addcslashes($E, '%_') . "%"; - $q.= "AND Email LIKE " . $dbh->quote($E) . " "; - $search_vars[] = "E"; - } - if ($R) { - $R = "%" . addcslashes($R, '%_') . "%"; - $q.= "AND RealName LIKE " . $dbh->quote($R) . " "; - $search_vars[] = "R"; - } - if ($I) { - $I = "%" . addcslashes($I, '%_') . "%"; - $q.= "AND IRCNick LIKE " . $dbh->quote($I) . " "; - $search_vars[] = "I"; - } - if ($K) { - $K = "%" . addcslashes(str_replace(" ", "", $K), '%_') . "%"; - $q.= "AND PGPKey LIKE " . $dbh->quote($K) . " "; - $search_vars[] = "K"; - } - switch ($SB) { - case 't': - $q.= "ORDER BY AccountTypeID, Username "; - break; - case 'r': - $q.= "ORDER BY RealName, AccountTypeID "; - break; - case 'i': - $q.= "ORDER BY IRCNick, AccountTypeID "; - break; - default: - $q.= "ORDER BY Username, AccountTypeID "; - break; - } - $search_vars[] = "SB"; - $q.= "LIMIT " . $HITS_PER_PAGE . " OFFSET " . $OFFSET; - - $dbh = DB::connect(); - - $result = $dbh->query($q); - - $userinfo = array(); - if ($result) { - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $userinfo[] = $row; - } - } - - include("account_search_results.php"); - return; -} - -/** - * Attempt to login and generate a session - * - * @return array Session ID for user, error message if applicable - */ -function try_login() { - $login_error = ""; - $new_sid = ""; - $userID = null; - - if (!isset($_REQUEST['user']) && !isset($_REQUEST['passwd'])) { - return array('SID' => '', 'error' => null); - } - - if (is_ipbanned()) { - $login_error = __('The login form is currently disabled ' . - 'for your IP address, probably due ' . - 'to sustained spam attacks. Sorry for the ' . - 'inconvenience.'); - return array('SID' => '', 'error' => $login_error); - } - - $dbh = DB::connect(); - $userID = uid_from_loginname($_REQUEST['user']); - - if (user_suspended($userID)) { - $login_error = __('Account suspended'); - return array('SID' => '', 'error' => $login_error); - } - - switch (check_passwd($userID, $_REQUEST['passwd'])) { - case -1: - $login_error = __('Your password has been reset. ' . - 'If you just created a new account, please ' . - 'use the link from the confirmation email ' . - 'to set an initial password. Otherwise, ' . - 'please request a reset key on the %s' . - 'Password Reset%s page.', '', - ''); - return array('SID' => '', 'error' => $login_error); - case 0: - $login_error = __("Bad username or password."); - return array('SID' => '', 'error' => $login_error); - case 1: - break; - } - - $logged_in = 0; - $num_tries = 0; - - /* Generate a session ID and store it. */ - while (!$logged_in && $num_tries < 5) { - $new_sid = new_sid(); - $q = "INSERT INTO Sessions (UsersID, SessionID, LastUpdateTS)" - ." VALUES (" . $userID . ", '" . $new_sid . "', " . strval(time()) . ")"; - $result = $dbh->exec($q); - - /* Query will fail if $new_sid is not unique. */ - if ($result) { - $logged_in = 1; - break; - } - - $num_tries++; - } - - if (!$logged_in) { - $login_error = __('An error occurred trying to generate a user session.'); - return array('SID' => $new_sid, 'error' => $login_error); - } - - $q = "UPDATE Users SET LastLogin = " . strval(time()) . ", "; - $q.= "LastLoginIPAddress = " . $dbh->quote($_SERVER['REMOTE_ADDR']) . " "; - $q.= "WHERE ID = $userID"; - $dbh->exec($q); - - /* Set the SID cookie. */ - if (isset($_POST['remember_me']) && $_POST['remember_me'] == "on") { - /* Set cookies for 30 days. */ - $timeout = config_get_int('options', 'persistent_cookie_timeout'); - $cookie_time = time() + $timeout; - - /* Set session for 30 days. */ - $q = "UPDATE Sessions SET LastUpdateTS = $cookie_time "; - $q.= "WHERE SessionID = '$new_sid'"; - $dbh->exec($q); - } else { - $cookie_time = 0; - } - - setcookie("AURSID", $new_sid, $cookie_time, "/", null, !empty($_SERVER['HTTPS']), true); - - $referer = in_request('referer'); - if (strpos($referer, aur_location()) !== 0) { - $referer = '/'; - } - header("Location: " . get_uri($referer)); - $login_error = ""; - return array('SID' => $new_sid, 'error' => null); -} - -/** - * Determine if the user is using a banned IP address - * - * @return bool True if IP address is banned, otherwise false - */ -function is_ipbanned() { - $dbh = DB::connect(); - - $q = "SELECT * FROM Bans WHERE IPAddress = " . $dbh->quote($_SERVER['REMOTE_ADDR']); - $result = $dbh->query($q); - - return ($result->fetchColumn() ? true : false); -} - -/** - * Validate a username against a collection of rules - * - * The username must be longer or equal to the configured minimum length. It - * must be shorter or equal to the configured maximum length. It must start and - * end with either a letter or a number. It can contain one period, hypen, or - * underscore. Returns boolean of whether name is valid. - * - * @param string $user Username to validate - * - * @return bool True if username meets criteria, otherwise false - */ -function valid_username($user) { - $length_min = config_get_int('options', 'username_min_len'); - $length_max = config_get_int('options', 'username_max_len'); - - if (strlen($user) < $length_min || strlen($user) > $length_max) { - return false; - } else if (!preg_match("/^[a-z0-9]+[.\-_]?[a-z0-9]+$/Di", $user)) { - return false; - } - - return true; -} - -/** - * Determine if a user already has a proposal open about themselves - * - * @param string $user Username to checkout for open proposal - * - * @return bool True if there is an open proposal about the user, otherwise false - */ -function open_user_proposals($user) { - $dbh = DB::connect(); - $q = "SELECT * FROM TU_VoteInfo WHERE User = " . $dbh->quote($user) . " "; - $q.= "AND End > " . strval(time()); - $result = $dbh->query($q); - - return ($result->fetchColumn() ? true : false); -} - -/** - * Add a new Trusted User proposal to the database - * - * @param string $agenda The agenda of the vote - * @param string $user The use the vote is about - * @param int $votelength The length of time for the vote to last - * @param string $submitteruid The user ID of the individual who submitted the proposal - * - * @return void - */ -function add_tu_proposal($agenda, $user, $votelength, $quorum, $submitteruid) { - $dbh = DB::connect(); - - $q = "SELECT COUNT(*) FROM Users WHERE (AccountTypeID = 2 OR AccountTypeID = 4)"; - $result = $dbh->query($q); - $row = $result->fetch(PDO::FETCH_NUM); - $active_tus = $row[0]; - - $q = "INSERT INTO TU_VoteInfo (Agenda, User, Submitted, End, Quorum, "; - $q.= "SubmitterID, ActiveTUs) VALUES "; - $q.= "(" . $dbh->quote($agenda) . ", " . $dbh->quote($user) . ", "; - $q.= strval(time()) . ", " . strval(time()) . " + " . $dbh->quote($votelength); - $q.= ", " . $dbh->quote($quorum) . ", " . $submitteruid . ", "; - $q.= $active_tus . ")"; - $result = $dbh->exec($q); -} - -/** - * Add a reset key to the database for a specified user - * - * @param string $resetkey A password reset key to be stored in database - * @param string $uid The user ID to store the reset key for - * - * @return void - */ -function create_resetkey($resetkey, $uid) { - $dbh = DB::connect(); - $q = "UPDATE Users "; - $q.= "SET ResetKey = '" . $resetkey . "' "; - $q.= "WHERE ID = " . $uid; - $dbh->exec($q); -} - -/** - * Send a reset key to a specific e-mail address - * - * @param string $user User name or email address of the user - * @param bool $welcome Whether to use the welcome message - * - * @return void - */ -function send_resetkey($user, $welcome=false) { - $uid = uid_from_loginname($user); - if ($uid == null) { - return; - } - - /* We (ab)use new_sid() to get a random 32 characters long string. */ - $resetkey = new_sid(); - create_resetkey($resetkey, $uid); - - /* Send e-mail with confirmation link. */ - notify(array($welcome ? 'welcome' : 'send-resetkey', $uid)); -} - -/** - * Change a user's password in the database if reset key and e-mail are correct - * - * @param string $password The new password - * @param string $resetkey Code e-mailed to a user to reset a password - * @param string $user User name or email address of the user - * - * @return string|void Redirect page if successful, otherwise return error message - */ -function password_reset($password, $resetkey, $user) { - $hash = password_hash($password, PASSWORD_DEFAULT); - - $dbh = DB::connect(); - $q = "UPDATE Users SET "; - $q.= "Passwd = " . $dbh->quote($hash) . ", "; - $q.= "ResetKey = '' "; - $q.= "WHERE ResetKey != '' "; - $q.= "AND ResetKey = " . $dbh->quote($resetkey) . " "; - $q.= "AND (Email = " . $dbh->quote($user) . " OR "; - $q.= "UserName = " . $dbh->quote($user) . ")"; - $result = $dbh->exec($q); - - if (!$result) { - $error = __('Invalid e-mail and reset key combination.'); - return $error; - } else { - header('Location: ' . get_uri('/passreset/') . '?step=complete'); - exit(); - } -} - -/** - * Determine if the password is longer than the minimum length - * - * @param string $passwd The password to check - * - * @return bool True if longer than minimum length, otherwise false - */ -function good_passwd($passwd) { - $length_min = config_get_int('options', 'passwd_min_len'); - return (strlen($passwd) >= $length_min); -} - -/** - * Determine if the password is correct and salt it if it hasn't been already - * - * @param int $user_id The user ID to check the password against - * @param string $passwd The password the visitor sent - * - * @return int Positive if password is correct, negative if password is unset - */ -function check_passwd($user_id, $passwd) { - $dbh = DB::connect(); - - /* Get password hash and salt. */ - $q = "SELECT Passwd, Salt FROM Users WHERE ID = " . intval($user_id); - $result = $dbh->query($q); - if (!$result) { - return 0; - } - $row = $result->fetch(PDO::FETCH_ASSOC); - if (!$row) { - return 0; - } - $hash = $row['Passwd']; - $salt = $row['Salt']; - if (!$hash) { - return -1; - } - - /* Verify the password hash. */ - if (!password_verify($passwd, $hash)) { - /* Invalid password, fall back to MD5. */ - if (md5($salt . $passwd) != $hash) { - return 0; - } - } - - /* Password correct, migrate the hash if necessary. */ - if (password_needs_rehash($hash, PASSWORD_DEFAULT)) { - $hash = password_hash($passwd, PASSWORD_DEFAULT); - - $q = "UPDATE Users SET Passwd = " . $dbh->quote($hash) . " "; - $q.= "WHERE ID = " . intval($user_id); - $dbh->query($q); - } - - return 1; -} - -/** - * Determine if the PGP key fingerprint is valid (must be 40 hexadecimal digits) - * - * @param string $fingerprint PGP fingerprint to check if valid - * - * @return bool True if the fingerprint is 40 hexadecimal digits, otherwise false - */ -function valid_pgp_fingerprint($fingerprint) { - $fingerprint = str_replace(" ", "", $fingerprint); - return (strlen($fingerprint) == 40 && ctype_xdigit($fingerprint)); -} - -/** - * Determine if the SSH public key is valid - * - * @param string $pubkey SSH public key to check - * - * @return bool True if the SSH public key is valid, otherwise false - */ -function valid_ssh_pubkey($pubkey) { - $valid_prefixes = explode(' ', config_get('auth', 'valid-keytypes')); - - $has_valid_prefix = false; - foreach ($valid_prefixes as $prefix) { - if (strpos($pubkey, $prefix . " ") === 0) { - $has_valid_prefix = true; - break; - } - } - if (!$has_valid_prefix) { - return false; - } - - $tokens = explode(" ", $pubkey); - if (empty($tokens[1])) { - return false; - } - - return (base64_encode(base64_decode($tokens[1], true)) == $tokens[1]); -} - -/** - * Determine if the user account has been suspended - * - * @param string $id The ID of user to check if suspended - * - * @return bool True if the user is suspended, otherwise false - */ -function user_suspended($id) { - $dbh = DB::connect(); - if (!$id) { - return false; - } - $q = "SELECT Suspended FROM Users WHERE ID = " . $id; - $result = $dbh->query($q); - if ($result) { - $row = $result->fetch(PDO::FETCH_NUM); - if ($row[0]) { - return true; - } - } - return false; -} - -/** - * Delete a specified user account from the database - * - * @param int $id The user ID of the account to be deleted - * - * @return void - */ -function user_delete($id) { - $dbh = DB::connect(); - $id = intval($id); - - /* - * These are normally already taken care of by propagation constraints - * but it is better to be explicit here. - */ - $fields_delete = array( - array("Sessions", "UsersID"), - array("PackageVotes", "UsersID"), - array("PackageNotifications", "UserID") - ); - - $fields_set_null = array( - array("PackageBases", "SubmitterUID"), - array("PackageBases", "MaintainerUID"), - array("PackageBases", "PackagerUID"), - array("PackageComments", "UsersID"), - array("PackageComments", "DelUsersID"), - array("PackageRequests", "UsersID"), - array("TU_VoteInfo", "SubmitterID"), - array("TU_Votes", "UserID") - ); - - foreach($fields_delete as list($table, $field)) { - $q = "DELETE FROM " . $table . " "; - $q.= "WHERE " . $field . " = " . $id; - $dbh->query($q); - } - - foreach($fields_set_null as list($table, $field)) { - $q = "UPDATE " . $table . " SET " . $field . " = NULL "; - $q.= "WHERE " . $field . " = " . $id; - $dbh->query($q); - } - - $q = "DELETE FROM Users WHERE ID = " . $id; - $dbh->query($q); - return; -} - -/** - * Remove the session from the database on logout - * - * @param string $sid User's session ID - * - * @return void - */ -function delete_session_id($sid) { - $dbh = DB::connect(); - - $q = "DELETE FROM Sessions WHERE SessionID = " . $dbh->quote($sid); - $dbh->query($q); -} - -/** - * Remove all sessions belonging to a particular user - * - * @param int $uid ID of user to remove all sessions for - * - * @return void - */ -function delete_user_sessions($uid) { - $dbh = DB::connect(); - - $q = "DELETE FROM Sessions WHERE UsersID = " . intval($uid); - $dbh->exec($q); -} - -/** - * Remove sessions from the database that have exceed the timeout - * - * @return void - */ -function clear_expired_sessions() { - $dbh = DB::connect(); - - $timeout = config_get_int('options', 'login_timeout'); - $q = "DELETE FROM Sessions WHERE LastUpdateTS < (" . strval(time()) . " - " . $timeout . ")"; - $dbh->query($q); - - return; -} - -/** - * Get account details for a specific user - * - * @param string $uid The User ID of account to get information for - * @param string $username The username of the account to get for - * - * @return array Account details for the specified user - */ -function account_details($uid, $username) { - $dbh = DB::connect(); - $q = "SELECT Users.*, AccountTypes.AccountType "; - $q.= "FROM Users, AccountTypes "; - $q.= "WHERE AccountTypes.ID = Users.AccountTypeID "; - if (!empty($uid)) { - $q.= "AND Users.ID = ".intval($uid); - } else { - $q.= "AND Users.Username = " . $dbh->quote($username); - } - $result = $dbh->query($q); - - if ($result) { - $row = $result->fetch(PDO::FETCH_ASSOC); - } - - return $row; -} - -/** - * Determine if a user has already voted on a specific proposal - * - * @param string $voteid The ID of the Trusted User proposal - * @param string $uid The ID to check if the user already voted - * - * @return bool True if the user has already voted, otherwise false - */ -function tu_voted($voteid, $uid) { - $dbh = DB::connect(); - - $q = "SELECT COUNT(*) FROM TU_Votes "; - $q.= "WHERE VoteID = " . intval($voteid) . " AND UserID = " . intval($uid); - $result = $dbh->query($q); - if ($result->fetchColumn() > 0) { - return true; - } - else { - return false; - } -} - -/** - * Get all current Trusted User proposals from the database - * - * @param string $order Ascending or descending order for the proposal listing - * - * @return array The details for all current Trusted User proposals - */ -function current_proposal_list($order) { - $dbh = DB::connect(); - - $q = "SELECT * FROM TU_VoteInfo WHERE End > " . time() . " ORDER BY Submitted " . $order; - $result = $dbh->query($q); - - $details = array(); - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $details[] = $row; - } - - return $details; -} - -/** - * Get a subset of all past Trusted User proposals from the database - * - * @param string $order Ascending or descending order for the proposal listing - * @param string $lim The number of proposals to list with the offset - * - * @return array The details for the subset of past Trusted User proposals - */ -function past_proposal_list($order, $lim) { - $dbh = DB::connect(); - - $q = "SELECT * FROM TU_VoteInfo WHERE End < " . time() . " ORDER BY Submitted " . $order . $lim; - $result = $dbh->query($q); - - $details = array(); - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $details[] = $row; - } - - return $details; -} - -/** - * Get the vote ID of the last vote of all Trusted Users - * - * @return array The vote ID of the last vote of each Trusted User - */ -function last_votes_list() { - $dbh = DB::connect(); - - $q = "SELECT UserID, MAX(VoteID) AS LastVote FROM TU_Votes, "; - $q .= "TU_VoteInfo, Users WHERE TU_VoteInfo.ID = TU_Votes.VoteID AND "; - $q .= "TU_VoteInfo.End < " . strval(time()) . " AND "; - $q .= "Users.ID = TU_Votes.UserID AND (Users.AccountTypeID = 2 OR Users.AccountTypeID = 4) "; - $q .= "GROUP BY UserID ORDER BY LastVote DESC, UserName ASC"; - $result = $dbh->query($q); - - $details = array(); - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $details[] = $row; - } - - return $details; -} - -/** - * Determine the total number of Trusted User proposals - * - * @return string The total number of Trusted User proposals - */ -function proposal_count() { - $dbh = DB::connect(); - $q = "SELECT COUNT(*) FROM TU_VoteInfo"; - $result = $dbh->query($q); - $row = $result->fetch(PDO::FETCH_NUM); - - return $row[0]; -} - -/** - * Get all details related to a specific vote from the database - * - * @param string $voteid The ID of the Trusted User proposal - * - * @return array All stored details for a specific vote - */ -function vote_details($voteid) { - $dbh = DB::connect(); - - $q = "SELECT * FROM TU_VoteInfo "; - $q.= "WHERE ID = " . intval($voteid); - - $result = $dbh->query($q); - $row = $result->fetch(PDO::FETCH_ASSOC); - - return $row; -} - -/** - * Get an alphabetical list of users who voted for a proposal with HTML links - * - * @param string $voteid The ID of the Trusted User proposal - * - * @return array All users who voted for a specific proposal - */ -function voter_list($voteid) { - $dbh = DB::connect(); - - $whovoted = array(); - - $q = "SELECT tv.UserID,U.Username "; - $q.= "FROM TU_Votes tv, Users U "; - $q.= "WHERE tv.VoteID = " . intval($voteid); - $q.= " AND tv.UserID = U.ID "; - $q.= "ORDER BY Username"; - - $result = $dbh->query($q); - if ($result) { - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $whovoted[] = $row['Username']; - } - } - return $whovoted; -} - -/** - * Cast a vote for a specific user proposal - * - * @param string $voteid The ID of the proposal being voted on - * @param string $uid The user ID of the individual voting - * @param string $vote Vote position, either "Yes", "No", or "Abstain" - * @param int $newtotal The total number of votes after the user has voted - * - * @return void - */ -function cast_proposal_vote($voteid, $uid, $vote, $newtotal) { - $dbh = DB::connect(); - - $q = "UPDATE TU_VoteInfo SET " . $vote . " = (" . $newtotal . ") WHERE ID = " . $voteid; - $result = $dbh->exec($q); - - $q = "INSERT INTO TU_Votes (VoteID, UserID) VALUES (" . intval($voteid) . ", " . intval($uid) . ")"; - $result = $dbh->exec($q); -} - -/** - * Verify a user has the proper permissions to edit an account - * - * @param array $acctinfo User account information for edited account - * - * @return bool True if permission to edit the account, otherwise false - */ -function can_edit_account($acctinfo) { - if ($acctinfo['AccountType'] == 'Developer' || - $acctinfo['AccountType'] == 'Trusted User & Developer') { - return has_credential(CRED_ACCOUNT_EDIT_DEV); - } - - $uid = $acctinfo['ID']; - return has_credential(CRED_ACCOUNT_EDIT, array($uid)); -} - -/* - * Compute the fingerprint of an SSH key. - * - * @param string $ssh_key The SSH public key to retrieve the fingerprint for - * - * @return string The SSH key fingerprint - */ -function ssh_key_fingerprint($ssh_key) { - $tmpfile = tempnam(sys_get_temp_dir(), "aurweb"); - file_put_contents($tmpfile, $ssh_key); - - /* - * The -l option of ssh-keygen can be used to show the fingerprint of - * the specified public key file. Expected output format: - * - * 2048 SHA256:uBBTXmCNjI2CnLfkuz9sG8F+e9/T4C+qQQwLZWIODBY user@host (RSA) - * - * ... where 2048 is the key length, the second token is the actual - * fingerprint, followed by the key comment and the key type. - */ - - $cmd = "/usr/bin/ssh-keygen -l -f " . escapeshellarg($tmpfile); - exec($cmd, $out, $ret); - if ($ret !== 0 || count($out) !== 1) { - return false; - } - - unlink($tmpfile); - - $tokens = explode(' ', $out[0]); - if (count($tokens) < 4) { - return false; - } - - $tokens = explode(':', $tokens[1]); - if (count($tokens) != 2 || $tokens[0] != 'SHA256') { - return false; - } - - return $tokens[1]; -} - -/* - * Get the SSH public keys associated with an account. - * - * @param int $uid The user ID of the account to retrieve the keys for. - * - * @return array An array representing the keys - */ -function account_get_ssh_keys($uid) { - $dbh = DB::connect(); - $q = "SELECT PubKey FROM SSHPubKeys WHERE UserID = " . intval($uid); - $result = $dbh->query($q); - - if ($result) { - return $result->fetchAll(PDO::FETCH_COLUMN, 0); - } else { - return array(); - } -} - -/* - * Set the SSH public keys associated with an account. - * - * @param int $uid The user ID of the account to assign the keys to. - * @param array $ssh_keys The SSH public keys. - * @param array $ssh_fingerprints The corresponding SSH key fingerprints. - * - * @return bool Boolean flag indicating success or failure. - */ -function account_set_ssh_keys($uid, $ssh_keys, $ssh_fingerprints) { - $dbh = DB::connect(); - - $q = sprintf("DELETE FROM SSHPubKeys WHERE UserID = %d", $uid); - $dbh->exec($q); - - $ssh_fingerprint = reset($ssh_fingerprints); - foreach ($ssh_keys as $ssh_key) { - $q = sprintf( - "INSERT INTO SSHPubKeys (UserID, Fingerprint, PubKey) " . - "VALUES (%d, %s, %s)", $uid, - $dbh->quote($ssh_fingerprint), $dbh->quote($ssh_key) - ); - $dbh->exec($q); - $ssh_fingerprint = next($ssh_fingerprints); - } - - return true; -} - -/* - * Invoke the email notification script. - * - * @param string $params Command line parameters for the script. - * - * @return void - */ -function notify($params) { - $cmd = config_get('notifications', 'notify-cmd'); - foreach ($params as $param) { - $cmd .= ' ' . escapeshellarg($param); - } - - $descspec = array( - 0 => array('pipe', 'r'), - 1 => array('pipe', 'w'), - ); - - $p = proc_open($cmd, $descspec, $pipes); - - if (!is_resource($p)) { - return false; - } - - fclose($pipes[0]); - fclose($pipes[1]); - - return proc_close($p); -} - -/* - * Obtain a list of terms a given user has not yet accepted. - * - * @param int $uid The ID of the user to obtain terms for. - * - * @return array A list of terms the user has not yet accepted. - */ -function fetch_updated_terms($uid) { - $dbh = DB::connect(); - - $q = "SELECT ID, Terms.Revision, Description, URL "; - $q .= "FROM Terms LEFT JOIN AcceptedTerms "; - $q .= "ON AcceptedTerms.TermsID = Terms.ID "; - $q .= "AND AcceptedTerms.UsersID = " . intval($uid) . " "; - $q .= "WHERE AcceptedTerms.Revision IS NULL OR "; - $q .= "AcceptedTerms.Revision < Terms.Revision"; - - $result = $dbh->query($q); - - if ($result) { - return $result->fetchAll(); - } else { - return array(); - } -} - -/* - * Accept a list of given terms. - * - * @param int $uid The ID of the user to accept the terms. - * @param array $termrev An array mapping each term to the accepted revision. - * - * @return void - */ -function accept_terms($uid, $termrev) { - $dbh = DB::connect(); - - $q = "SELECT TermsID, Revision FROM AcceptedTerms "; - $q .= "WHERE UsersID = " . intval($uid); - - $result = $dbh->query($q); - - if (!$result) { - return; - } - - $termrev_update = array(); - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $id = $row['TermsID']; - if (!array_key_exists($id, $termrev)) { - continue; - } - if ($row['Revision'] < $termrev[$id]) { - $termrev_update[$id] = $termrev[$id]; - } - } - $termrev_add = array_diff_key($termrev, $termrev_update); - - foreach ($termrev_add as $id => $rev) { - $q = "INSERT INTO AcceptedTerms (TermsID, UsersID, Revision) "; - $q .= "VALUES (" . intval($id) . ", " . intval($uid) . ", "; - $q .= intval($rev) . ")"; - $dbh->exec($q); - } - - foreach ($termrev_update as $id => $rev) { - $q = "UPDATE AcceptedTerms "; - $q .= "SET Revision = " . intval($rev) . " "; - $q .= "WHERE TermsID = " . intval($id) . " AND "; - $q .= "UsersID = " . intval($uid); - $dbh->exec($q); - } -} - -function account_comments($uid, $limit, $offset=0) { - $dbh = DB::connect(); - $q = "SELECT PackageComments.ID, Comments, UsersID, "; - $q.= "PackageBaseId, CommentTS, DelTS, EditedTS, B.UserName AS EditUserName, "; - $q.= "PinnedTS, "; - $q.= "C.UserName as DelUserName, RenderedComment, "; - $q.= "PB.ID as PackageBaseID, PB.Name as PackageBaseName "; - $q.= "FROM PackageComments "; - $q.= "LEFT JOIN PackageBases PB ON PackageComments.PackageBaseID = PB.ID "; - $q.= "LEFT JOIN Users A ON PackageComments.UsersID = A.ID "; - $q.= "LEFT JOIN Users B ON PackageComments.EditedUsersID = B.ID "; - $q.= "LEFT JOIN Users C ON PackageComments.DelUsersID = C.ID "; - $q.= "WHERE A.ID = " . $dbh->quote($uid) . " "; - $q.= "ORDER BY CommentTS DESC"; - - if ($limit > 0) { - $q.=" LIMIT " . intval($limit); - } - - if ($offset > 0) { - $q.=" OFFSET " . intval($offset); - } - - $result = $dbh->query($q); - if (!$result) { - return null; - } - - return $result->fetchAll(); -} - -function account_comments_count($uid) { - $dbh = DB::connect(); - $q = "SELECT COUNT(*) "; - $q.= "FROM PackageComments "; - $q.= "LEFT JOIN Users A ON PackageComments.UsersID = A.ID "; - $q.= "WHERE A.ID = " . $dbh->quote($uid); - - $result = $dbh->query($q); - return $result->fetchColumn(); -} - -/* - * Compute the list of active CAPTCHA salts. The salt changes based on the - * number of registered users. This ensures that new users always use a - * different salt and protects against hardcoding the CAPTCHA response. - * - * The first CAPTCHA in the list is the most recent one and should be used for - * new CAPTCHA challenges. The other ones are slightly outdated but may still - * be valid for recent challenges that were created before the number of users - * increased. The current implementation ensures that we can still use our - * CAPTCHA salt, even if five new users registered since the CAPTCHA challenge - * was created. - * - * @return string The list of active salts, the first being the most recent - * one. - */ -function get_captcha_salts() { - $dbh = DB::connect(); - $q = "SELECT count(*) FROM Users"; - $result = $dbh->query($q); - $user_count = $result->fetchColumn(); - - $ret = array(); - for ($i = 0; $i <= 5; $i++) { - array_push($ret, 'aurweb-' . ($user_count - $i)); - } - return $ret; -} - -/* - * Return the CAPTCHA challenge for a given salt. - * - * @param string $salt The salt to be used for the CAPTCHA computation. - * - * @return string The challenge as a string. - */ -function get_captcha_challenge($salt) { - $token = substr(md5($salt), 0, 3); - return "LC_ALL=C pacman -V|sed -r 's#[0-9]+#" . $token . "#g'|md5sum|cut -c1-6"; -} - -/* - * Compute CAPTCHA answer for a given salt. - * - * @param string $salt The salt to be used for the CAPTCHA computation. - * - * @return string The correct answer as a string. - */ -function get_captcha_answer($salt) { - $token = substr(md5($salt), 0, 3); - $text = <<quote($_COOKIE["AURSID"]); - $result = $dbh->query($q); - $row = $result->fetch(PDO::FETCH_NUM); - - if (!$row) { - # Invalid SessionID - hacker alert! - # - $failed = 1; - } else { - $last_update = $row[0]; - if ($last_update + $timeout <= $row[1]) { - $failed = 2; - } - } - - if ($failed == 1) { - # clear out the hacker's cookie, and send them to a naughty page - # why do you have to be so harsh on these people!? - # - setcookie("AURSID", "", 1, "/", null, !empty($_SERVER['HTTPS']), true); - unset($_COOKIE['AURSID']); - } elseif ($failed == 2) { - # session id timeout was reached and they must login again. - # - delete_session_id($_COOKIE["AURSID"]); - - setcookie("AURSID", "", 1, "/", null, !empty($_SERVER['HTTPS']), true); - unset($_COOKIE['AURSID']); - } else { - # still logged in and haven't reached the timeout, go ahead - # and update the idle timestamp - - # Only update the timestamp if it is less than the - # current time plus $timeout. - # - # This keeps 'remembered' sessions from being - # overwritten. - if ($last_update < time() + $timeout) { - $q = "UPDATE Sessions SET LastUpdateTS = " . strval(time()) . " "; - $q.= "WHERE SessionID = " . $dbh->quote($_COOKIE["AURSID"]); - $dbh->exec($q); - } - } - } - return; -} - -/** - * Redirect user to the Terms of Service agreement if there are updated terms. - * - * @return void - */ -function check_tos() { - if (!isset($_COOKIE["AURSID"])) { - return; - } - - $path = $_SERVER['PATH_INFO']; - $route = get_route($path); - if (!$route || $route == "tos.php") { - return; - } - - if (count(fetch_updated_terms(uid_from_sid($_COOKIE["AURSID"]))) > 0) { - header('Location: ' . get_uri('/tos')); - exit(); - } -} - -/** - * Verify the supplied CSRF token matches expected token - * - * @return bool True if the CSRF token is the same as the cookie SID, otherwise false - */ -function check_token() { - if (isset($_POST['token']) && isset($_COOKIE['AURSID'])) { - return ($_POST['token'] == $_COOKIE['AURSID']); - } else { - return false; - } -} - -/** - * Verify a user supplied e-mail against RFC 3696 and DNS records - * - * @param string $addy E-mail address being validated in foo@example.com format - * - * @return bool True if e-mail passes validity checks, otherwise false - */ -function valid_email($addy) { - // check against RFC 3696 - if (filter_var($addy, FILTER_VALIDATE_EMAIL) === false) { - return false; - } - - // check dns for mx, a, aaaa records - list($local, $domain) = explode('@', $addy); - if (!(checkdnsrr($domain, 'MX') || checkdnsrr($domain, 'A') || checkdnsrr($domain, 'AAAA'))) { - return false; - } - - return true; -} - -/** - * Verify that a given URL is valid and uses the HTTP(s) protocol - * - * @param string $url URL of the home page to be validated - * - * @return bool True if URL passes validity checks, false otherwise - */ -function valid_homepage($url) { - if (filter_var($url, FILTER_VALIDATE_URL) === false) { - return false; - } - - $url_components = parse_url($url); - if (!in_array($url_components['scheme'], array('http', 'https'))) { - return false; - } - - return true; -} - -/** - * Generate a unique session ID - * - * @return string MD5 hash of the concatenated user IP, random number, and current time - */ -function new_sid() { - return md5($_SERVER['REMOTE_ADDR'] . uniqid(mt_rand(), true)); -} - -/** - * Determine the user's username in the database using a user ID - * - * @param string $id User's ID - * - * @return string Username if it exists, otherwise null - */ -function username_from_id($id) { - $id = intval($id); - - $dbh = DB::connect(); - $q = "SELECT Username FROM Users WHERE ID = " . $dbh->quote($id); - $result = $dbh->query($q); - if (!$result) { - return null; - } - - $row = $result->fetch(PDO::FETCH_NUM); - if ($row) { - return $row[0]; - } -} - -/** - * Determine the user's username in the database using a session ID - * - * @param string $sid User's session ID - * - * @return string Username of the visitor - */ -function username_from_sid($sid="") { - if (!$sid) { - return ""; - } - $dbh = DB::connect(); - $q = "SELECT Username "; - $q.= "FROM Users, Sessions "; - $q.= "WHERE Users.ID = Sessions.UsersID "; - $q.= "AND Sessions.SessionID = " . $dbh->quote($sid); - $result = $dbh->query($q); - if (!$result) { - return ""; - } - $row = $result->fetch(PDO::FETCH_NUM); - - if ($row) { - return $row[0]; - } -} - -/** - * Format a user name for inclusion in HTML data - * - * @param string $username The user name to format - * - * @return string The generated HTML code for the account link - */ -function html_format_username($username) { - $username_fmt = $username ? htmlspecialchars($username, ENT_QUOTES) : __("None"); - - if ($username && isset($_COOKIE["AURSID"])) { - $link = '' . $username_fmt . ''; - return $link; - } else { - return $username_fmt; - } -} - -/** - * Format the maintainer and co-maintainers for inclusion in HTML data - * - * @param string $maintainer The user name of the maintainer - * @param array $comaintainers The list of co-maintainer user names - * - * @return string The generated HTML code for the account links - */ -function html_format_maintainers($maintainer, $comaintainers) { - $code = html_format_username($maintainer); - - if (count($comaintainers) > 0) { - $code .= ' ('; - foreach ($comaintainers as $comaintainer) { - $code .= html_format_username($comaintainer); - if ($comaintainer !== end($comaintainers)) { - $code .= ', '; - } - } - $code .= ')'; - } - - return $code; -} - -/** - * Format a link in the package actions box - * - * @param string $uri The link target - * @param string $inner The HTML code to use for the link label - * - * @return string The generated HTML code for the action link - */ -function html_action_link($uri, $inner) { - if (isset($_COOKIE["AURSID"])) { - $code = ''; - } else { - $code = ''; - } - $code .= $inner . ''; - - return $code; -} - -/** - * Format a form in the package actions box - * - * @param string $uri The link target - * @param string $action The action name (passed as HTTP POST parameter) - * @param string $inner The HTML code to use for the link label - * - * @return string The generated HTML code for the action link - */ -function html_action_form($uri, $action, $inner) { - if (isset($_COOKIE["AURSID"])) { - $code = '
    '; - $code .= ''; - $code .= '
    '; - } else { - $code = ''; - $code .= $inner . ''; - } - - return $code; -} - -/** - * Determine the user's e-mail address in the database using a session ID - * - * @param string $sid User's session ID - * - * @return string User's e-mail address as given during registration - */ -function email_from_sid($sid="") { - if (!$sid) { - return ""; - } - $dbh = DB::connect(); - $q = "SELECT Email "; - $q.= "FROM Users, Sessions "; - $q.= "WHERE Users.ID = Sessions.UsersID "; - $q.= "AND Sessions.SessionID = " . $dbh->quote($sid); - $result = $dbh->query($q); - if (!$result) { - return ""; - } - $row = $result->fetch(PDO::FETCH_NUM); - - if ($row) { - return $row[0]; - } -} - -/** - * Determine the user's account type in the database using a session ID - * - * @param string $sid User's session ID - * - * @return string Account type of user ("User", "Trusted User", or "Developer") - */ -function account_from_sid($sid="") { - if (!$sid) { - return ""; - } - $dbh = DB::connect(); - $q = "SELECT AccountType "; - $q.= "FROM Users, AccountTypes, Sessions "; - $q.= "WHERE Users.ID = Sessions.UsersID "; - $q.= "AND AccountTypes.ID = Users.AccountTypeID "; - $q.= "AND Sessions.SessionID = " . $dbh->quote($sid); - $result = $dbh->query($q); - if (!$result) { - return ""; - } - $row = $result->fetch(PDO::FETCH_NUM); - - if ($row) { - return $row[0]; - } -} - -/** - * Determine the user's ID in the database using a session ID - * - * @param string $sid User's session ID - * - * @return string|int The user's name, 0 on query failure - */ -function uid_from_sid($sid="") { - if (!$sid) { - return ""; - } - $dbh = DB::connect(); - $q = "SELECT Users.ID "; - $q.= "FROM Users, Sessions "; - $q.= "WHERE Users.ID = Sessions.UsersID "; - $q.= "AND Sessions.SessionID = " . $dbh->quote($sid); - $result = $dbh->query($q); - if (!$result) { - return 0; - } - $row = $result->fetch(PDO::FETCH_NUM); - - if ($row) { - return $row[0]; - } -} - -/** - * Common AUR header displayed on all pages - * - * @global string $LANG Language selected by the visitor - * @global array $SUPPORTED_LANGS Languages that are supported by the AUR - * @param string $title Name of the AUR page to be displayed on browser - * - * @return void - */ -function html_header($title="", $details=array()) { - global $LANG; - global $SUPPORTED_LANGS; - - include('header.php'); - return; -} - -/** - * Common AUR footer displayed on all pages - * - * @param string $ver The AUR version - * - * @return void - */ -function html_footer($ver="") { - include('footer.php'); - return; -} - -/** - * Determine if a user has permission to submit a package - * - * @param string $name Name of the package to be submitted - * @param string $sid User's session ID - * - * @return int 0 if the user can't submit, 1 if the user can submit - */ -function can_submit_pkgbase($name="", $sid="") { - if (!$name || !$sid) {return 0;} - $dbh = DB::connect(); - $q = "SELECT MaintainerUID "; - $q.= "FROM PackageBases WHERE Name = " . $dbh->quote($name); - $result = $dbh->query($q); - $row = $result->fetch(PDO::FETCH_NUM); - - if (!$row[0]) { - return 1; - } - $my_uid = uid_from_sid($sid); - - if ($row[0] === NULL || $row[0] == $my_uid) { - return 1; - } - - return 0; -} - -/** - * Determine if a package can be overwritten by some package base - * - * @param string $name Name of the package to be submitted - * @param int $base_id The ID of the package base - * - * @return bool True if the package can be overwritten, false if not - */ -function can_submit_pkg($name, $base_id) { - $dbh = DB::connect(); - $q = "SELECT COUNT(*) FROM Packages WHERE "; - $q.= "Name = " . $dbh->quote($name) . " AND "; - $q.= "PackageBaseID <> " . intval($base_id); - $result = $dbh->query($q); - - if (!$result) return false; - return ($result->fetchColumn() == 0); -} - -/** - * Recursively delete a directory - * - * @param string $dirname Name of the directory to be removed - * - * @return void - */ -function rm_tree($dirname) { - if (empty($dirname) || !is_dir($dirname)) return; - - foreach (scandir($dirname) as $item) { - if ($item != '.' && $item != '..') { - $path = $dirname . '/' . $item; - if (is_file($path) || is_link($path)) { - unlink($path); - } - else { - rm_tree($path); - } - } - } - - rmdir($dirname); - - return; -} - - /** - * Determine the user's ID in the database using a username - * - * @param string $username The username of an account - * - * @return string Return user ID if exists for username, otherwise null - */ -function uid_from_username($username) { - $dbh = DB::connect(); - $q = "SELECT ID FROM Users WHERE Username = " . $dbh->quote($username); - $result = $dbh->query($q); - if (!$result) { - return null; - } - - $row = $result->fetch(PDO::FETCH_NUM); - if ($row) { - return $row[0]; - } -} - -/** - * Determine the user's ID in the database using a username or email address - * - * @param string $username The username or email address of an account - * - * @return string Return user ID if exists, otherwise null - */ -function uid_from_loginname($loginname) { - $uid = uid_from_username($loginname); - if (!$uid) { - $uid = uid_from_email($loginname); - } - return $uid; -} - -/** - * Determine the user's ID in the database using an e-mail address - * - * @param string $email An e-mail address in foo@example.com format - * - * @return string The user's ID - */ -function uid_from_email($email) { - $dbh = DB::connect(); - $q = "SELECT ID FROM Users WHERE Email = " . $dbh->quote($email); - $result = $dbh->query($q); - if (!$result) { - return null; - } - - $row = $result->fetch(PDO::FETCH_NUM); - if ($row) { - return $row[0]; - } -} - -/** - * Generate clean url with edited/added user values - * - * Makes a clean string of variables for use in URLs based on current $_GET and - * list of values to edit/add to that. Any empty variables are discarded. - * - * @example print "http://example.com/test.php?" . mkurl("foo=bar&bar=baz") - * - * @param string $append string of variables and values formatted as in URLs - * - * @return string clean string of variables to append to URL, urlencoded - */ -function mkurl($append) { - $get = $_GET; - $append = explode('&', $append); - $uservars = array(); - $out = ''; - - foreach ($append as $i) { - $ex = explode('=', $i); - $uservars[$ex[0]] = $ex[1]; - } - - foreach ($uservars as $k => $v) { $get[$k] = $v; } - - foreach ($get as $k => $v) { - if ($v !== '') { - $out .= '&' . urlencode($k) . '=' . urlencode($v); - } - } - - return substr($out, 5); -} - -/** - * Get a package comment - * - * @param int $comment_id The ID of the comment - * - * @return array The user ID and comment OR null, null in case of an error - */ -function comment_by_id($comment_id) { - $dbh = DB::connect(); - $q = "SELECT UsersID, Comments FROM PackageComments "; - $q.= "WHERE ID = " . intval($comment_id); - $result = $dbh->query($q); - if (!$result) { - return array(null, null); - } - - return $result->fetch(PDO::FETCH_NUM); -} - -/** - * Process submitted comments so any links can be followed - * - * @param string $comment Raw user submitted package comment - * - * @return string User comment with links printed in HTML - */ -function parse_comment($comment) { - $url_pattern = '/(\b(?:https?|ftp):\/\/[\w\/\#~:.?+=&%@!\-;,]+?' . - '(?=[.:?\-;,]*(?:[^\w\/\#~:.?+=&%@!\-;,]|$)))/iS'; - - $matches = preg_split($url_pattern, $comment, -1, - PREG_SPLIT_DELIM_CAPTURE); - - $html = ''; - for ($i = 0; $i < count($matches); $i++) { - if ($i % 2) { - # convert links - $html .= '' . htmlspecialchars($matches[$i]) . ''; - } - else { - # convert everything else - $html .= nl2br(htmlspecialchars($matches[$i])); - } - } - - return $html; -} - -/** - * Wrapper for beginning a database transaction - */ -function begin_atomic_commit() { - $dbh = DB::connect(); - $dbh->beginTransaction(); -} - -/** - * Wrapper for committing a database transaction - */ -function end_atomic_commit() { - $dbh = DB::connect(); - $dbh->commit(); -} - -/** - * Merge pkgbase and package options - * - * Merges entries of the first and the second array. If any key appears in both - * arrays and the corresponding value in the second array is either a non-array - * type or a non-empty array, the value from the second array replaces the - * value from the first array. If the value from the second array is an array - * containing a single empty string, the value in the resulting array becomes - * an empty array instead. If the value in the second array is empty, the - * resulting array contains the value from the first array. - * - * @param array $pkgbase_info Options from the pkgbase section - * @param array $section_info Options from the package section - * - * @return array Merged information from both sections - */ -function array_pkgbuild_merge($pkgbase_info, $section_info) { - $pi = $pkgbase_info; - foreach ($section_info as $opt_key => $opt_val) { - if (is_array($opt_val)) { - if ($opt_val == array('')) { - $pi[$opt_key] = array(); - } elseif (count($opt_val) > 0) { - $pi[$opt_key] = $opt_val; - } - } else { - $pi[$opt_key] = $opt_val; - } - } - return $pi; -} - -/** - * Bound an integer value between two values - * - * @param int $n Integer value to bound - * @param int $min Lower bound - * @param int $max Upper bound - * - * @return int Bounded integer value - */ -function bound($n, $min, $max) { - return min(max($n, $min), $max); -} - -/** - * Return the URL of the AUR root - * - * @return string The URL of the AUR root - */ -function aur_location() { - $location = config_get('options', 'aur_location'); - if (substr($location, -1) != '/') { - $location .= '/'; - } - return $location; -} - -/** - * Calculate pagination templates - * - * @return array The array of pagination templates, per page, and offset values - */ -function calculate_pagination($total_comment_count) { - /* Sanitize paging variables. */ - if (isset($_GET["O"])) { - $_GET["O"] = max(intval($_GET["O"]), 0); - } else { - $_GET["O"] = 0; - } - $offset = $_GET["O"]; - - if (isset($_GET["PP"])) { - $_GET["PP"] = bound(intval($_GET["PP"]), 1, 250); - } else { - $_GET["PP"] = 10; - } - $per_page = $_GET["PP"]; - - // Page offsets start at zero, so page 2 has offset 1, which means that we - // need to add 1 to the offset to get the current page. - $current_page = ceil($offset / $per_page) + 1; - $num_pages = ceil($total_comment_count / $per_page); - $pagination_templs = array(); - - if ($current_page > 1) { - $previous_page = $current_page - 1; - $previous_offset = ($previous_page - 1) * $per_page; - $pagination_templs['« ' . __('First')] = 0; - $pagination_templs['‹ ' . __('Previous')] = $previous_offset; - } - - if ($current_page - 5 > 1) { - $pagination_templs["..."] = false; - } - - for ($i = max($current_page - 5, 1); $i <= min($num_pages, $current_page + 5); $i++) { - $pagination_templs[$i] = ($i - 1) * $per_page; - } - - if ($current_page + 5 < $num_pages) - $pagination_templs["... "] = false; - - if ($current_page < $num_pages) { - $pagination_templs[__('Next') . ' ›'] = $current_page * $per_page; - $pagination_templs[__('Last') . ' »'] = ($num_pages - 1) * $per_page; - } - - return array($pagination_templs, $per_page, $offset); -} diff --git a/web/lib/aurjson.class.php b/web/lib/aurjson.class.php deleted file mode 100644 index e7bc7f97..00000000 --- a/web/lib/aurjson.class.php +++ /dev/null @@ -1,711 +0,0 @@ -version = intval($http_data['v']); - } - if ($this->version < 1 || $this->version > 6) { - return $this->json_error('Invalid version specified.'); - } - - if (!isset($http_data['type']) || !isset($http_data['arg'])) { - return $this->json_error('No request type/data specified.'); - } - if (!in_array($http_data['type'], self::$exposed_methods)) { - return $this->json_error('Incorrect request type specified.'); - } - - if (isset($http_data['search_by']) && !isset($http_data['by'])) { - $http_data['by'] = $http_data['search_by']; - } - if (isset($http_data['by']) && !in_array($http_data['by'], self::$exposed_fields)) { - return $this->json_error('Incorrect by field specified.'); - } - - $this->dbh = DB::connect(); - - if ($this->check_ratelimit($_SERVER['REMOTE_ADDR'])) { - header("HTTP/1.1 429 Too Many Requests"); - return $this->json_error('Rate limit reached'); - } - - $type = str_replace('-', '_', $http_data['type']); - if ($type == 'info' && $this->version >= 5) { - $type = 'multiinfo'; - } - $json = call_user_func(array(&$this, $type), $http_data); - - $etag = md5($json); - header("Etag: \"$etag\""); - /* - * Make sure to strip a few things off the - * if-none-match header. Stripping whitespace may not - * be required, but removing the quote on the incoming - * header is required to make the equality test. - */ - $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? - trim($_SERVER['HTTP_IF_NONE_MATCH'], "\t\n\r\" ") : false; - if ($if_none_match && $if_none_match == $etag) { - header('HTTP/1.1 304 Not Modified'); - return; - } - - if (isset($http_data['callback'])) { - $callback = $http_data['callback']; - if (!preg_match('/^[a-zA-Z0-9()_.]{1,128}$/D', $callback)) { - return $this->json_error('Invalid callback name.'); - } - header('content-type: text/javascript'); - return '/**/' . $callback . '(' . $json . ')'; - } else { - header('content-type: application/json'); - return $json; - } - } - - /* - * Check if an IP needs to be rate limited. - * - * @param $ip IP of the current request - * - * @return true if IP needs to be rate limited, false otherwise. - */ - private function check_ratelimit($ip) { - $limit = config_get("ratelimit", "request_limit"); - if ($limit == 0) { - return false; - } - - $this->update_ratelimit($ip); - - $status = false; - $value = get_cache_value('ratelimit:' . $ip, $status); - if (!$status) { - $stmt = $this->dbh->prepare(" - SELECT Requests FROM ApiRateLimit - WHERE IP = :ip"); - $stmt->bindParam(":ip", $ip); - $result = $stmt->execute(); - - if (!$result) { - return false; - } - - $row = $stmt->fetch(PDO::FETCH_ASSOC); - $value = $row['Requests']; - } - - return $value > $limit; - } - - /* - * Update a rate limit for an IP by increasing it's requests value by one. - * - * @param $ip IP of the current request - * - * @return void - */ - private function update_ratelimit($ip) { - $window_length = config_get("ratelimit", "window_length"); - $db_backend = config_get("database", "backend"); - $time = time(); - $deletion_time = $time - $window_length; - - /* Try to use the cache. */ - $status = false; - $value = get_cache_value('ratelimit-ws:' . $ip, $status); - if (!$status || ($status && $value < $deletion_time)) { - if (set_cache_value('ratelimit-ws:' . $ip, $time, $window_length) && - set_cache_value('ratelimit:' . $ip, 1, $window_length)) { - return; - } - } else { - $value = get_cache_value('ratelimit:' . $ip, $status); - if ($status && set_cache_value('ratelimit:' . $ip, $value + 1, $window_length)) - return; - } - - /* Clean up old windows. */ - $stmt = $this->dbh->prepare(" - DELETE FROM ApiRateLimit - WHERE WindowStart < :time"); - $stmt->bindParam(":time", $deletion_time); - $stmt->execute(); - - if ($db_backend == "mysql") { - $stmt = $this->dbh->prepare(" - INSERT INTO ApiRateLimit - (IP, Requests, WindowStart) - VALUES (:ip, 1, :window_start) - ON DUPLICATE KEY UPDATE Requests=Requests+1"); - $stmt->bindParam(":ip", $ip); - $stmt->bindParam(":window_start", $time); - $stmt->execute(); - } elseif ($db_backend == "sqlite") { - $stmt = $this->dbh->prepare(" - INSERT OR IGNORE INTO ApiRateLimit - (IP, Requests, WindowStart) - VALUES (:ip, 0, :window_start);"); - $stmt->bindParam(":ip", $ip); - $stmt->bindParam(":window_start", $time); - $stmt->execute(); - - $stmt = $this->dbh->prepare(" - UPDATE ApiRateLimit - SET Requests = Requests + 1 - WHERE IP = :ip"); - $stmt->bindParam(":ip", $ip); - $stmt->execute(); - } else { - throw new RuntimeException("Unknown database backend"); - } - } - - /* - * Returns a JSON formatted error string. - * - * @param $msg The error string to return - * - * @return mixed A json formatted error response. - */ - private function json_error($msg) { - header('content-type: application/json'); - if ($this->version < 3) { - return $this->json_results('error', 0, $msg, NULL); - } elseif ($this->version >= 3) { - return $this->json_results('error', 0, array(), $msg); - } - } - - /* - * Returns a JSON formatted result data. - * - * @param $type The response method type. - * @param $count The number of results to return - * @param $data The result data to return - * @param $error An error message to include in the response - * - * @return mixed A json formatted result response. - */ - private function json_results($type, $count, $data, $error) { - $json_array = array( - 'version' => $this->version, - 'type' => $type, - 'resultcount' => $count, - 'results' => $data - ); - - if ($this->version != 5) { - $json_array['warning'] = 'The use of versions lower than 5 is ' - . 'now deprecated and will soon be unsupported. To ensure ' - . 'your API client supports the change without issue, it ' - . 'should use version 5 and adjust for any changes in the ' - . 'API interface. See https://aur.archlinux.org/rpc for ' - . 'documentation related to v5.'; - } - - if ($error) { - $json_array['error'] = $error; - } - - return json_encode($json_array); - } - - /* - * Get extended package details (for info and multiinfo queries). - * - * @param $pkgid The ID of the package to retrieve details for. - * @param $base_id The ID of the package base to retrieve details for. - * - * @return array An array containing package details. - */ - private function get_extended_fields($pkgid, $base_id) { - $query = "SELECT DependencyTypes.Name AS Type, " . - "PackageDepends.DepName AS Name, " . - "PackageDepends.DepCondition AS Cond " . - "FROM PackageDepends " . - "LEFT JOIN DependencyTypes " . - "ON DependencyTypes.ID = PackageDepends.DepTypeID " . - "WHERE PackageDepends.PackageID = " . $pkgid . " " . - "UNION SELECT RelationTypes.Name AS Type, " . - "PackageRelations.RelName AS Name, " . - "PackageRelations.RelCondition AS Cond " . - "FROM PackageRelations " . - "LEFT JOIN RelationTypes " . - "ON RelationTypes.ID = PackageRelations.RelTypeID " . - "WHERE PackageRelations.PackageID = " . $pkgid . " " . - "UNION SELECT 'groups' AS Type, `Groups`.`Name`, '' AS Cond " . - "FROM `Groups` INNER JOIN PackageGroups " . - "ON PackageGroups.PackageID = " . $pkgid . " " . - "AND PackageGroups.GroupID = `Groups`.ID " . - "UNION SELECT 'license' AS Type, Licenses.Name, '' AS Cond " . - "FROM Licenses INNER JOIN PackageLicenses " . - "ON PackageLicenses.PackageID = " . $pkgid . " " . - "AND PackageLicenses.LicenseID = Licenses.ID"; - $ttl = config_get_int('options', 'cache_pkginfo_ttl'); - $rows = db_cache_result($query, 'extended-fields:' . $pkgid, PDO::FETCH_ASSOC, $ttl); - - $type_map = array( - 'depends' => 'Depends', - 'makedepends' => 'MakeDepends', - 'checkdepends' => 'CheckDepends', - 'optdepends' => 'OptDepends', - 'conflicts' => 'Conflicts', - 'provides' => 'Provides', - 'replaces' => 'Replaces', - 'groups' => 'Groups', - 'license' => 'License', - ); - $data = array(); - foreach ($rows as $row) { - $type = $type_map[$row['Type']]; - $data[$type][] = $row['Name'] . $row['Cond']; - } - - if ($this->version >= 5) { - $query = "SELECT Keyword FROM PackageKeywords " . - "WHERE PackageBaseID = " . intval($base_id) . " " . - "ORDER BY Keyword ASC"; - $ttl = config_get_int('options', 'cache_pkginfo_ttl'); - $rows = db_cache_result($query, 'keywords:' . intval($base_id), PDO::FETCH_NUM, $ttl); - $data['Keywords'] = array_map(function ($x) { return $x[0]; }, $rows); - } - - return $data; - } - - /* - * Retrieve package information (used in info, multiinfo, search and - * depends requests). - * - * @param $type The request type. - * @param $where_condition An SQL WHERE-condition to filter packages. - * - * @return mixed Returns an array of package matches. - */ - private function process_query($type, $where_condition) { - $max_results = config_get_int('options', 'max_rpc_results'); - - if ($this->version == 1) { - $fields = implode(',', self::$fields_v1); - $query = "SELECT {$fields} " . - "FROM Packages LEFT JOIN PackageBases " . - "ON PackageBases.ID = Packages.PackageBaseID " . - "LEFT JOIN Users " . - "ON PackageBases.MaintainerUID = Users.ID " . - "LEFT JOIN PackageLicenses " . - "ON PackageLicenses.PackageID = Packages.ID " . - "LEFT JOIN Licenses " . - "ON Licenses.ID = PackageLicenses.LicenseID " . - "WHERE ${where_condition} " . - "AND PackageBases.PackagerUID IS NOT NULL " . - "LIMIT $max_results"; - } elseif ($this->version >= 2) { - if ($this->version == 2 || $this->version == 3) { - $fields = implode(',', self::$fields_v2); - } else if ($this->version >= 4 && $this->version <= 6) { - $fields = implode(',', self::$fields_v4); - } - $query = "SELECT {$fields} " . - "FROM Packages LEFT JOIN PackageBases " . - "ON PackageBases.ID = Packages.PackageBaseID " . - "LEFT JOIN Users " . - "ON PackageBases.MaintainerUID = Users.ID " . - "WHERE ${where_condition} " . - "AND PackageBases.PackagerUID IS NOT NULL " . - "LIMIT $max_results"; - } - $result = $this->dbh->query($query); - - if ($result) { - $resultcount = 0; - $search_data = array(); - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $resultcount++; - $row['URLPath'] = sprintf(config_get('options', 'snapshot_uri'), urlencode($row['PackageBase'])); - if ($this->version < 4) { - $row['CategoryID'] = 1; - } - - /* - * Unfortunately, mysql_fetch_assoc() returns - * all fields as strings. We need to coerce - * numeric values into integers to provide - * proper data types in the JSON response. - */ - foreach (self::$numeric_fields as $field) { - if (isset($row[$field])) { - $row[$field] = intval($row[$field]); - } - } - - foreach (self::$decimal_fields as $field) { - if (isset($row[$field])) { - $row[$field] = floatval($row[$field]); - } - } - - if ($this->version >= 2 && ($type == 'info' || $type == 'multiinfo')) { - $extfields = $this->get_extended_fields($row['ID'], $row['PackageBaseID']); - if ($extfields) { - $row = array_merge($row, $extfields); - } - } - - if ($this->version < 3) { - if ($type == 'info') { - $search_data = $row; - break; - } else { - array_push($search_data, $row); - } - } elseif ($this->version >= 3) { - array_push($search_data, $row); - } - } - - if ($resultcount === $max_results) { - return $this->json_error('Too many package results.'); - } - - return $this->json_results($type, $resultcount, $search_data, NULL); - } else { - return $this->json_results($type, 0, array(), NULL); - } - } - - /* - * Parse the args to the multiinfo function. We may have a string or an - * array, so do the appropriate thing. Within the elements, both * package - * IDs and package names are valid; sort them into the relevant arrays and - * escape/quote the names. - * - * @param array $args Query parameters. - * - * @return mixed An array containing 'ids' and 'names'. - */ - private function parse_multiinfo_args($args) { - if (!is_array($args)) { - $args = array($args); - } - - $id_args = array(); - $name_args = array(); - foreach ($args as $arg) { - if (!$arg) { - continue; - } - if ($this->version < 5 && is_numeric($arg)) { - $id_args[] = intval($arg); - } else { - $name_args[] = $this->dbh->quote($arg); - } - } - - return array('ids' => $id_args, 'names' => $name_args); - } - - /* - * Performs a fulltext mysql search of the package database. - * - * @param array $http_data Query parameters. - * - * @return mixed Returns an array of package matches. - */ - private function search($http_data) { - $keyword_string = $http_data['arg']; - - if (isset($http_data['by'])) { - $search_by = $http_data['by']; - } else { - $search_by = 'name-desc'; - } - - if ($search_by === 'name' || $search_by === 'name-desc') { - if (strlen($keyword_string) < 2) { - return $this->json_error('Query arg too small.'); - } - - if ($this->version >= 6 && $search_by === 'name-desc') { - $where_condition = construct_keyword_search($this->dbh, - $keyword_string, true, false); - } else { - $keyword_string = $this->dbh->quote( - "%" . addcslashes($keyword_string, '%_') . "%"); - - if ($search_by === 'name') { - $where_condition = "(Packages.Name LIKE $keyword_string)"; - } else if ($search_by === 'name-desc') { - $where_condition = "(Packages.Name LIKE $keyword_string "; - $where_condition .= "OR Description LIKE $keyword_string)"; - } - - } - } else if ($search_by === 'maintainer') { - if (empty($keyword_string)) { - $where_condition = "Users.ID is NULL"; - } else { - $keyword_string = $this->dbh->quote($keyword_string); - $where_condition = "Users.Username = $keyword_string "; - } - } else if (in_array($search_by, self::$exposed_depfields)) { - if (empty($keyword_string)) { - return $this->json_error('Query arg is empty.'); - } else { - $keyword_string = $this->dbh->quote($keyword_string); - $search_by = $this->dbh->quote($search_by); - $subquery = "SELECT PackageDepends.DepName FROM PackageDepends "; - $subquery .= "LEFT JOIN DependencyTypes "; - $subquery .= "ON PackageDepends.DepTypeID = DependencyTypes.ID "; - $subquery .= "WHERE PackageDepends.PackageID = Packages.ID "; - $subquery .= "AND DependencyTypes.Name = $search_by"; - $where_condition = "$keyword_string IN ($subquery)"; - } - } - - return $this->process_query('search', $where_condition); - } - - /* - * Returns the info on a specific package. - * - * @param array $http_data Query parameters. - * - * @return mixed Returns an array of value data containing the package data - */ - private function info($http_data) { - $pqdata = $http_data['arg']; - if ($this->version < 5 && is_numeric($pqdata)) { - $where_condition = "Packages.ID = $pqdata"; - } else { - $where_condition = "Packages.Name = " . $this->dbh->quote($pqdata); - } - - return $this->process_query('info', $where_condition); - } - - /* - * Returns the info on multiple packages. - * - * @param array $http_data Query parameters. - * - * @return mixed Returns an array of results containing the package data - */ - private function multiinfo($http_data) { - $pqdata = $http_data['arg']; - $args = $this->parse_multiinfo_args($pqdata); - $ids = $args['ids']; - $names = $args['names']; - - if (!$ids && !$names) { - return $this->json_error('Invalid query arguments.'); - } - - $where_condition = ""; - if ($ids) { - $ids_value = implode(',', $args['ids']); - $where_condition .= "Packages.ID IN ($ids_value) "; - } - if ($ids && $names) { - $where_condition .= "OR "; - } - if ($names) { - /* - * Individual names were quoted in - * parse_multiinfo_args(). - */ - $names_value = implode(',', $args['names']); - $where_condition .= "Packages.Name IN ($names_value) "; - } - - return $this->process_query('multiinfo', $where_condition); - } - - /* - * Returns all the packages for a specific maintainer. - * - * @param array $http_data Query parameters. - * - * @return mixed Returns an array of value data containing the package data - */ - private function msearch($http_data) { - $http_data['by'] = 'maintainer'; - return $this->search($http_data); - } - - /* - * Get all package names that start with $search. - * - * @param array $http_data Query parameters. - * - * @return string The JSON formatted response data. - */ - private function suggest($http_data) { - $search = $http_data['arg']; - $query = "SELECT Packages.Name FROM Packages "; - $query.= "LEFT JOIN PackageBases "; - $query.= "ON PackageBases.ID = Packages.PackageBaseID "; - $query.= "WHERE Packages.Name LIKE "; - $query.= $this->dbh->quote(addcslashes($search, '%_') . '%'); - $query.= " AND PackageBases.PackagerUID IS NOT NULL "; - $query.= "ORDER BY Name ASC LIMIT 20"; - - $result = $this->dbh->query($query); - $result_array = array(); - - if ($result) { - $result_array = $result->fetchAll(PDO::FETCH_COLUMN, 0); - } - - return json_encode($result_array); - } - - /* - * Get all package base names that start with $search. - * - * @param array $http_data Query parameters. - * - * @return string The JSON formatted response data. - */ - private function suggest_pkgbase($http_data) { - $search = $http_data['arg']; - $query = "SELECT Name FROM PackageBases WHERE Name LIKE "; - $query.= $this->dbh->quote(addcslashes($search, '%_') . '%'); - $query.= " AND PackageBases.PackagerUID IS NOT NULL "; - $query.= "ORDER BY Name ASC LIMIT 20"; - - $result = $this->dbh->query($query); - $result_array = array(); - - if ($result) { - $result_array = $result->fetchAll(PDO::FETCH_COLUMN, 0); - } - - return json_encode($result_array); - } - - /** - * Get the HTML markup of the comment form. - * - * @param array $http_data Query parameters. - * - * @return string The JSON formatted response data. - */ - private function get_comment_form($http_data) { - if (!isset($http_data['base_id']) || !isset($http_data['pkgbase_name'])) { - $output = array( - 'success' => 0, - 'error' => __('Package base ID or package base name missing.') - ); - return json_encode($output); - } - - $comment_id = intval($http_data['arg']); - $base_id = intval($http_data['base_id']); - $pkgbase_name = $http_data['pkgbase_name']; - - list($user_id, $comment) = comment_by_id($comment_id); - - if (!has_credential(CRED_COMMENT_EDIT, array($user_id))) { - $output = array( - 'success' => 0, - 'error' => __('You are not allowed to edit this comment.') - ); - return json_encode($output); - } elseif (is_null($comment)) { - $output = array( - 'success' => 0, - 'error' => __('Comment does not exist.') - ); - return json_encode($output); - } - - ob_start(); - include('pkg_comment_form.php'); - $html = ob_get_clean(); - $output = array( - 'success' => 1, - 'form' => $html - ); - - return json_encode($output); - } -} - diff --git a/web/lib/cachefuncs.inc.php b/web/lib/cachefuncs.inc.php deleted file mode 100644 index b2b96c24..00000000 --- a/web/lib/cachefuncs.inc.php +++ /dev/null @@ -1,99 +0,0 @@ -addServer($mcserver[0], intval($mcserver[1])); - } -} - -# Set a value in the cache (currently APC) if cache is available for use. If -# not available, this becomes effectively a no-op (return value is -# false). Accepts an optional TTL (defaults to 600 seconds). -function set_cache_value($key, $value, $ttl=600) { - $status = false; - if (defined('EXTENSION_LOADED_APC')) { - $status = apc_store(CACHE_PREFIX.$key, $value, $ttl); - } - if (defined('EXTENSION_LOADED_MEMCACHE')) { - global $memcache; - $status = $memcache->set(CACHE_PREFIX.$key, $value, $ttl); - } - return $status; -} - -# Get a value from the cache (currently APC) if cache is available for use. If -# not available, this returns false (optionally sets passed in variable $status -# to false, much like apc_fetch() behaves). This allows for testing the fetch -# result appropriately even in the event that a 'false' value was the value in -# the cache. -function get_cache_value($key, &$status=false) { - if(defined('EXTENSION_LOADED_APC')) { - $ret = apc_fetch(CACHE_PREFIX.$key, $status); - if ($status) { - return $ret; - } - } - if (defined('EXTENSION_LOADED_MEMCACHE')) { - global $memcache; - $ret = $memcache->get(CACHE_PREFIX.$key); - if (!$ret) { - $status = false; - } - else { - $status = true; - } - return $ret; - } - return $status; -} - -# Run a simple db query, retrieving and/or caching the value if APC is -# available for use. Accepts an optional TTL value (defaults to 600 seconds). -function db_cache_value($dbq, $key, $ttl=600) { - $dbh = DB::connect(); - $status = false; - $value = get_cache_value($key, $status); - if (!$status) { - $result = $dbh->query($dbq); - if (!$result) { - return false; - } - $row = $result->fetch(PDO::FETCH_NUM); - $value = $row[0]; - set_cache_value($key, $value, $ttl); - } - return $value; -} - -# Run a simple db query, retrieving and/or caching the result set if APC is -# available for use. Accepts an optional TTL value (defaults to 600 seconds). -function db_cache_result($dbq, $key, $fetch_style=PDO::FETCH_NUM, $ttl=600) { - $dbh = DB::connect(); - $status = false; - $value = get_cache_value($key, $status); - if (!$status) { - $result = $dbh->query($dbq); - if (!$result) { - return false; - } - $value = $result->fetchAll($fetch_style); - set_cache_value($key, $value, $ttl); - } - return $value; -} - -?> diff --git a/web/lib/confparser.inc.php b/web/lib/confparser.inc.php deleted file mode 100644 index fdd2b78e..00000000 --- a/web/lib/confparser.inc.php +++ /dev/null @@ -1,59 +0,0 @@ -useCached(); // use cached version if age<1 hour -$rss->title = "PHP news"; -$rss->description = "daily news from the PHP scripting world"; - -//optional -$rss->descriptionTruncSize = 500; -$rss->descriptionHtmlSyndicated = true; - -$rss->link = "http://www.dailyphp.net/news"; -$rss->syndicationURL = "http://www.dailyphp.net/".$_SERVER["PHP_SELF"]; - -$image = new FeedImage(); -$image->title = "dailyphp.net logo"; -$image->url = "http://www.dailyphp.net/images/logo.gif"; -$image->link = "http://www.dailyphp.net"; -$image->description = "Feed provided by dailyphp.net. Click to visit."; - -//optional -$image->descriptionTruncSize = 500; -$image->descriptionHtmlSyndicated = true; - -$rss->image = $image; - -// get your news items from somewhere, e.g. your database: -mysql_select_db($dbHost, $dbUser, $dbPass); -$res = mysql_query("SELECT * FROM news ORDER BY newsdate DESC"); -while ($data = mysql_fetch_object($res)) { - $item = new FeedItem(); - $item->title = $data->title; - $item->link = $data->url; - $item->description = $data->short; - - //optional - item->descriptionTruncSize = 500; - item->descriptionHtmlSyndicated = true; - - $item->date = $data->newsdate; - $item->source = "http://www.dailyphp.net"; - $item->author = "John Doe"; - - $rss->addItem($item); -} - -// valid format strings are: RSS0.91, RSS1.0, RSS2.0, PIE0.1 (deprecated), -// MBOX, OPML, ATOM, ATOM0.3, HTML, JS -echo $rss->saveFeed("RSS1.0", "news/feed.xml"); - - -*************************************************************************** -* A little setup * -**************************************************************************/ - -// your local timezone, set to "" to disable or for GMT -define("TIME_ZONE","+01:00"); - - - - -/** - * Version string. - **/ -define("FEEDCREATOR_VERSION", "FeedCreator 1.7.2"); - - - -/** - * A FeedItem is a part of a FeedCreator feed. - * - * @author Kai Blankenhorn - * @since 1.3 - */ -class FeedItem extends HtmlDescribable { - /** - * Mandatory attributes of an item. - */ - var $title, $description, $link; - - /** - * Optional attributes of an item. - */ - var $author, $authorEmail, $image, $category, $comments, $guid, $guidIsPermaLink, $source, $creator; - - /** - * Publishing date of an item. May be in one of the following formats: - * - * RFC 822: - * "Mon, 20 Jan 03 18:05:41 +0400" - * "20 Jan 03 18:05:41 +0000" - * - * ISO 8601: - * "2003-01-20T18:05:41+04:00" - * - * Unix: - * 1043082341 - */ - var $date; - - /** - * Any additional elements to include as an assiciated array. All $key => $value pairs - * will be included unencoded in the feed item in the form - * <$key>$value - * Again: No encoding will be used! This means you can invalidate or enhance the feed - * if $value contains markup. This may be abused to embed tags not implemented by - * the FeedCreator class used. - */ - var $additionalElements = Array(); - - // on hold - // var $source; -} - - - -/** - * An FeedImage may be added to a FeedCreator feed. - * @author Kai Blankenhorn - * @since 1.3 - */ -class FeedImage extends HtmlDescribable { - /** - * Mandatory attributes of an image. - */ - var $title, $url, $link; - - /** - * Optional attributes of an image. - */ - var $width, $height, $description; -} - - - -/** - * An HtmlDescribable is an item within a feed that can have a description that may - * include HTML markup. - */ -class HtmlDescribable { - /** - * Indicates whether the description field should be rendered in HTML. - */ - var $descriptionHtmlSyndicated; - - /** - * Indicates whether and to how many characters a description should be truncated. - */ - var $descriptionTruncSize; - - /** - * Returns a formatted description field, depending on descriptionHtmlSyndicated and - * $descriptionTruncSize properties - * @return string the formatted description - */ - function getDescription() { - $descriptionField = new FeedHtmlField($this->description); - $descriptionField->syndicateHtml = $this->descriptionHtmlSyndicated; - $descriptionField->truncSize = $this->descriptionTruncSize; - return $descriptionField->output(); - } - -} - - -/** - * An FeedHtmlField describes and generates - * a feed, item or image html field (probably a description). Output is - * generated based on $truncSize, $syndicateHtml properties. - * @author Pascal Van Hecke - * @version 1.6 - */ -class FeedHtmlField { - /** - * Mandatory attributes of a FeedHtmlField. - */ - var $rawFieldContent; - - /** - * Optional attributes of a FeedHtmlField. - * - */ - var $truncSize, $syndicateHtml; - - /** - * Creates a new instance of FeedHtmlField. - * @param $string: if given, sets the rawFieldContent property - */ - function FeedHtmlField($parFieldContent) { - if ($parFieldContent) { - $this->rawFieldContent = $parFieldContent; - } - } - - - /** - * Creates the right output, depending on $truncSize, $syndicateHtml properties. - * @return string the formatted field - */ - function output() { - // when field available and syndicated in html we assume - // - valid html in $rawFieldContent and we enclose in CDATA tags - // - no truncation (truncating risks producing invalid html) - if (!$this->rawFieldContent) { - $result = ""; - } elseif ($this->syndicateHtml) { - $result = "rawFieldContent."]]>"; - } else { - if ($this->truncSize and is_int($this->truncSize)) { - $result = FeedCreator::iTrunc(htmlspecialchars($this->rawFieldContent),$this->truncSize); - } else { - $result = htmlspecialchars($this->rawFieldContent); - } - } - return $result; - } - -} - - - -/** - * UniversalFeedCreator lets you choose during runtime which - * format to build. - * For general usage of a feed class, see the FeedCreator class - * below or the example above. - * - * @since 1.3 - * @author Kai Blankenhorn - */ -class UniversalFeedCreator extends FeedCreator { - var $_feed; - - function _setFormat($format) { - switch (strtoupper($format)) { - - case "2.0": - // fall through - case "RSS2.0": - $this->_feed = new RSSCreator20(); - break; - - case "1.0": - // fall through - case "RSS1.0": - $this->_feed = new RSSCreator10(); - break; - - case "0.91": - // fall through - case "RSS0.91": - $this->_feed = new RSSCreator091(); - break; - - case "PIE0.1": - $this->_feed = new PIECreator01(); - break; - - case "MBOX": - $this->_feed = new MBOXCreator(); - break; - - case "OPML": - $this->_feed = new OPMLCreator(); - break; - - case "ATOM": - // fall through: always the latest ATOM version - - case "ATOM0.3": - $this->_feed = new AtomCreator03(); - break; - - case "HTML": - $this->_feed = new HTMLCreator(); - break; - - case "JS": - // fall through - case "JAVASCRIPT": - $this->_feed = new JSCreator(); - break; - - default: - $this->_feed = new RSSCreator091(); - break; - } - - $vars = get_object_vars($this); - foreach ($vars as $key => $value) { - // prevent overwriting of properties "contentType", "encoding"; do not copy "_feed" itself - if (!in_array($key, array("_feed", "contentType", "encoding"))) { - $this->_feed->{$key} = $this->{$key}; - } - } - } - - /** - * Creates a syndication feed based on the items previously added. - * - * @see FeedCreator::addItem() - * @param string format format the feed should comply to. Valid values are: - * "PIE0.1", "mbox", "RSS0.91", "RSS1.0", "RSS2.0", "OPML", "ATOM0.3", "HTML", "JS" - * @return string the contents of the feed. - */ - function createFeed($format = "RSS0.91") { - $this->_setFormat($format); - return $this->_feed->createFeed(); - } - - - - /** - * Saves this feed as a file on the local disk. After the file is saved, an HTTP redirect - * header may be sent to redirect the use to the newly created file. - * @since 1.4 - * - * @param string format format the feed should comply to. Valid values are: - * "PIE0.1" (deprecated), "mbox", "RSS0.91", "RSS1.0", "RSS2.0", "OPML", "ATOM", "ATOM0.3", "HTML", "JS" - * @param string filename optional the filename where a recent version of the feed is saved. If not specified, the filename is $_SERVER["PHP_SELF"] with the extension changed to .xml (see _generateFilename()). - * @param boolean displayContents optional send the content of the file or not. If true, the file will be sent in the body of the response. - */ - function saveFeed($format="RSS0.91", $filename="", $displayContents=true) { - $this->_setFormat($format); - $this->_feed->saveFeed($filename, $displayContents); - } - - - /** - * Turns on caching and checks if there is a recent version of this feed in the cache. - * If there is, an HTTP redirect header is sent. - * To effectively use caching, you should create the FeedCreator object and call this method - * before anything else, especially before you do the time consuming task to build the feed - * (web fetching, for example). - * - * @param string format format the feed should comply to. Valid values are: - * "PIE0.1" (deprecated), "mbox", "RSS0.91", "RSS1.0", "RSS2.0", "OPML", "ATOM0.3". - * @param filename string optional the filename where a recent version of the feed is saved. If not specified, the filename is $_SERVER["PHP_SELF"] with the extension changed to .xml (see _generateFilename()). - * @param timeout int optional the timeout in seconds before a cached version is refreshed (defaults to 3600 = 1 hour) - */ - function useCached($format="RSS0.91", $filename="", $timeout=3600) { - $this->_setFormat($format); - $this->_feed->useCached($filename, $timeout); - } - -} - - -/** - * FeedCreator is the abstract base implementation for concrete - * implementations that implement a specific format of syndication. - * - * @abstract - * @author Kai Blankenhorn - * @since 1.4 - */ -class FeedCreator extends HtmlDescribable { - - /** - * Mandatory attributes of a feed. - */ - var $title, $description, $link; - - - /** - * Optional attributes of a feed. - */ - var $syndicationURL, $image, $language, $copyright, $pubDate, $lastBuildDate, $editor, $editorEmail, $webmaster, $category, $docs, $ttl, $rating, $skipHours, $skipDays; - - /** - * The url of the external xsl stylesheet used to format the naked rss feed. - * Ignored in the output when empty. - */ - var $xslStyleSheet = ""; - - - /** - * @access private - */ - var $items = Array(); - - - /** - * This feed's MIME content type. - * @since 1.4 - * @access private - */ - var $contentType = "application/xml"; - - - /** - * This feed's character encoding. - * @since 1.6.1 - **/ - var $encoding = "ISO-8859-1"; - - - /** - * Any additional elements to include as an assiciated array. All $key => $value pairs - * will be included unencoded in the feed in the form - * <$key>$value - * Again: No encoding will be used! This means you can invalidate or enhance the feed - * if $value contains markup. This may be abused to embed tags not implemented by - * the FeedCreator class used. - */ - var $additionalElements = Array(); - - - /** - * Adds an FeedItem to the feed. - * - * @param object FeedItem $item The FeedItem to add to the feed. - * @access public - */ - function addItem($item) { - $this->items[] = $item; - } - - - /** - * Truncates a string to a certain length at the most sensible point. - * First, if there's a '.' character near the end of the string, the string is truncated after this character. - * If there is no '.', the string is truncated after the last ' ' character. - * If the string is truncated, " ..." is appended. - * If the string is already shorter than $length, it is returned unchanged. - * - * @static - * @param string string A string to be truncated. - * @param int length the maximum length the string should be truncated to - * @return string the truncated string - */ - function iTrunc($string, $length) { - if (strlen($string)<=$length) { - return $string; - } - - $pos = strrpos($string,"."); - if ($pos>=$length-4) { - $string = substr($string,0,$length-4); - $pos = strrpos($string,"."); - } - if ($pos>=$length*0.4) { - return substr($string,0,$pos+1)." ..."; - } - - $pos = strrpos($string," "); - if ($pos>=$length-4) { - $string = substr($string,0,$length-4); - $pos = strrpos($string," "); - } - if ($pos>=$length*0.4) { - return substr($string,0,$pos)." ..."; - } - - return substr($string,0,$length-4)." ..."; - - } - - - /** - * Creates a comment indicating the generator of this feed. - * The format of this comment seems to be recognized by - * Syndic8.com. - */ - function _createGeneratorComment() { - return "\n"; - } - - - /** - * Creates a string containing all additional elements specified in - * $additionalElements. - * @param elements array an associative array containing key => value pairs - * @param indentString string a string that will be inserted before every generated line - * @return string the XML tags corresponding to $additionalElements - */ - function _createAdditionalElements($elements, $indentString="") { - $ae = ""; - if (is_array($elements)) { - foreach($elements AS $key => $value) { - $ae.= $indentString."<$key>$value\n"; - } - } - return $ae; - } - - function _createStylesheetReferences() { - $xml = ""; - if ($this->cssStyleSheet) $xml .= "cssStyleSheet."\" type=\"text/css\"?>\n"; - if ($this->xslStyleSheet) $xml .= "xslStyleSheet."\" type=\"text/xsl\"?>\n"; - return $xml; - } - - - /** - * Builds the feed's text. - * @abstract - * @return string the feed's complete text - */ - function createFeed() { - } - - /** - * Generate a filename for the feed cache file. The result will be $_SERVER["PHP_SELF"] with the extension changed to .xml. - * For example: - * - * echo $_SERVER["PHP_SELF"]."\n"; - * echo FeedCreator::_generateFilename(); - * - * would produce: - * - * /rss/latestnews.php - * latestnews.xml - * - * @return string the feed cache filename - * @since 1.4 - * @access private - */ - function _generateFilename() { - $fileInfo = pathinfo($_SERVER["PHP_SELF"]); - return substr($fileInfo["basename"],0,-(strlen($fileInfo["extension"])+1)).".xml"; - } - - - /** - * @since 1.4 - * @access private - */ - function _redirect($filename) { - // attention, heavily-commented-out-area - - // maybe use this in addition to file time checking - //Header("Expires: ".date("r",time()+$this->_timeout)); - - /* no caching at all, doesn't seem to work as good: - Header("Cache-Control: no-cache"); - Header("Pragma: no-cache"); - */ - - // HTTP redirect, some feed readers' simple HTTP implementations don't follow it - //Header("Location: ".$filename); - - Header("Content-Type: ".$this->contentType."; charset=".$this->encoding."; filename=".basename($filename)); - Header("Content-Disposition: inline; filename=".basename($filename)); - readfile($filename, "r"); - die(); - } - - /** - * Turns on caching and checks if there is a recent version of this feed in the cache. - * If there is, an HTTP redirect header is sent. - * To effectively use caching, you should create the FeedCreator object and call this method - * before anything else, especially before you do the time consuming task to build the feed - * (web fetching, for example). - * @since 1.4 - * @param filename string optional the filename where a recent version of the feed is saved. If not specified, the filename is $_SERVER["PHP_SELF"] with the extension changed to .xml (see _generateFilename()). - * @param timeout int optional the timeout in seconds before a cached version is refreshed (defaults to 3600 = 1 hour) - */ - function useCached($filename="", $timeout=3600) { - $this->_timeout = $timeout; - if ($filename=="") { - $filename = $this->_generateFilename(); - } - if (file_exists($filename) AND (time()-filemtime($filename) < $timeout)) { - $this->_redirect($filename); - } - } - - - /** - * Saves this feed as a file on the local disk. After the file is saved, a redirect - * header may be sent to redirect the user to the newly created file. - * @since 1.4 - * - * @param filename string optional the filename where a recent version of the feed is saved. If not specified, the filename is $_SERVER["PHP_SELF"] with the extension changed to .xml (see _generateFilename()). - * @param redirect boolean optional send an HTTP redirect header or not. If true, the user will be automatically redirected to the created file. - */ - function saveFeed($filename="", $displayContents=true) { - if ($filename=="") { - $filename = $this->_generateFilename(); - } - $feedFile = fopen($filename, "w+"); - if ($feedFile) { - fputs($feedFile,$this->createFeed()); - fclose($feedFile); - if ($displayContents) { - $this->_redirect($filename); - } - } else { - echo "
    Error creating feed file, please check write permissions.
    "; - } - } - -} - - -/** - * FeedDate is an internal class that stores a date for a feed or feed item. - * Usually, you won't need to use this. - */ -class FeedDate { - var $unix; - - /** - * Creates a new instance of FeedDate representing a given date. - * Accepts RFC 822, ISO 8601 date formats as well as unix time stamps. - * @param mixed $dateString optional the date this FeedDate will represent. If not specified, the current date and time is used. - */ - function FeedDate($dateString="") { - if ($dateString=="") $dateString = date("r"); - - if (is_integer($dateString)) { - $this->unix = $dateString; - return; - } - if (preg_match("~(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun),\\s+)?(\\d{1,2})\\s+([a-zA-Z]{3})\\s+(\\d{4})\\s+(\\d{2}):(\\d{2}):(\\d{2})\\s+(.*)~",$dateString,$matches)) { - $months = Array("Jan"=>1,"Feb"=>2,"Mar"=>3,"Apr"=>4,"May"=>5,"Jun"=>6,"Jul"=>7,"Aug"=>8,"Sep"=>9,"Oct"=>10,"Nov"=>11,"Dec"=>12); - $this->unix = mktime($matches[4],$matches[5],$matches[6],$months[$matches[2]],$matches[1],$matches[3]); - if (substr($matches[7],0,1)=='+' OR substr($matches[7],0,1)=='-') { - $tzOffset = (substr($matches[7],0,3) * 60 + substr($matches[7],-2)) * 60; - } else { - if (strlen($matches[7])==1) { - $oneHour = 3600; - $ord = ord($matches[7]); - if ($ord < ord("M")) { - $tzOffset = (ord("A") - $ord - 1) * $oneHour; - } elseif ($ord >= ord("M") AND $matches[7]!="Z") { - $tzOffset = ($ord - ord("M")) * $oneHour; - } elseif ($matches[7]=="Z") { - $tzOffset = 0; - } - } - switch ($matches[7]) { - case "UT": - case "GMT": $tzOffset = 0; - } - } - $this->unix += $tzOffset; - return; - } - if (preg_match("~(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(.*)~",$dateString,$matches)) { - $this->unix = mktime($matches[4],$matches[5],$matches[6],$matches[2],$matches[3],$matches[1]); - if (substr($matches[7],0,1)=='+' OR substr($matches[7],0,1)=='-') { - $tzOffset = (substr($matches[7],0,3) * 60 + substr($matches[7],-2)) * 60; - } else { - if ($matches[7]=="Z") { - $tzOffset = 0; - } - } - $this->unix += $tzOffset; - return; - } - $this->unix = 0; - } - - /** - * Gets the date stored in this FeedDate as an RFC 822 date. - * - * @return a date in RFC 822 format - */ - function rfc822() { - //return gmdate("r",$this->unix); - $date = gmdate("D, d M Y H:i:s", $this->unix); - if (TIME_ZONE!="") $date .= " ".str_replace(":","",TIME_ZONE); - return $date; - } - - /** - * Gets the date stored in this FeedDate as an ISO 8601 date. - * - * @return a date in ISO 8601 format - */ - function iso8601() { - $date = gmdate("Y-m-d\TH:i:sO",$this->unix); - $date = substr($date,0,22) . ':' . substr($date,-2); - if (TIME_ZONE!="") $date = str_replace("+00:00",TIME_ZONE,$date); - return $date; - } - - /** - * Gets the date stored in this FeedDate as unix time stamp. - * - * @return a date as a unix time stamp - */ - function unix() { - return $this->unix; - } -} - - -/** - * RSSCreator10 is a FeedCreator that implements RDF Site Summary (RSS) 1.0. - * - * @see http://www.purl.org/rss/1.0/ - * @since 1.3 - * @author Kai Blankenhorn - */ -class RSSCreator10 extends FeedCreator { - - /** - * Builds the RSS feed's text. The feed will be compliant to RDF Site Summary (RSS) 1.0. - * The feed will contain all items previously added in the same order. - * @return string the feed's complete text - */ - function createFeed() { - $feed = "encoding."\"?>\n"; - $feed.= $this->_createGeneratorComment(); - if ($this->cssStyleSheet=="") { - $cssStyleSheet = "http://www.w3.org/2000/08/w3c-synd/style.css"; - } - $feed.= $this->_createStylesheetReferences(); - $feed.= "\n"; - $feed.= " syndicationURL."\">\n"; - $feed.= " ".htmlspecialchars($this->title)."\n"; - $feed.= " ".htmlspecialchars($this->description)."\n"; - $feed.= " ".$this->link."\n"; - if ($this->image!=null) { - $feed.= " image->url."\" />\n"; - } - $now = new FeedDate(); - $feed.= " ".htmlspecialchars($now->iso8601())."\n"; - $feed.= " \n"; - $feed.= " \n"; - for ($i=0;$iitems);$i++) { - $feed.= " items[$i]->link)."\"/>\n"; - } - $feed.= " \n"; - $feed.= " \n"; - $feed.= " \n"; - if ($this->image!=null) { - $feed.= " image->url."\">\n"; - $feed.= " ".$this->image->title."\n"; - $feed.= " ".$this->image->link."\n"; - $feed.= " ".$this->image->url."\n"; - $feed.= " \n"; - } - $feed.= $this->_createAdditionalElements($this->additionalElements, " "); - - for ($i=0;$iitems);$i++) { - $feed.= " items[$i]->link)."\">\n"; - //$feed.= " Posting\n"; - $feed.= " text/html\n"; - if ($this->items[$i]->date!=null) { - $itemDate = new FeedDate($this->items[$i]->date); - $feed.= " ".htmlspecialchars($itemDate->iso8601())."\n"; - } - if ($this->items[$i]->source!="") { - $feed.= " ".htmlspecialchars($this->items[$i]->source)."\n"; - } - if ($this->items[$i]->author!="") { - $feed.= " ".htmlspecialchars($this->items[$i]->author)."\n"; - } - $feed.= " ".htmlspecialchars(strip_tags(strtr($this->items[$i]->title,"\n\r"," ")))."\n"; - $feed.= " ".htmlspecialchars($this->items[$i]->link)."\n"; - $feed.= " ".htmlspecialchars($this->items[$i]->description)."\n"; - $feed.= $this->_createAdditionalElements($this->items[$i]->additionalElements, " "); - $feed.= " \n"; - } - $feed.= "\n"; - return $feed; - } -} - - - -/** - * RSSCreator091 is a FeedCreator that implements RSS 0.91 Spec, revision 3. - * - * @see http://my.netscape.com/publish/formats/rss-spec-0.91.html - * @since 1.3 - * @author Kai Blankenhorn - */ -class RSSCreator091 extends FeedCreator { - - /** - * Stores this RSS feed's version number. - * @access private - */ - var $RSSVersion; - - function RSSCreator091() { - $this->_setRSSVersion("0.91"); - $this->contentType = "application/rss+xml"; - } - - /** - * Sets this RSS feed's version number. - * @access private - */ - function _setRSSVersion($version) { - $this->RSSVersion = $version; - } - - /** - * Builds the RSS feed's text. The feed will be compliant to RDF Site Summary (RSS) 1.0. - * The feed will contain all items previously added in the same order. - * @return string the feed's complete text - */ - function createFeed() { - $feed = "encoding."\"?>\n"; - $feed.= $this->_createGeneratorComment(); - $feed.= $this->_createStylesheetReferences(); - $feed.= "RSSVersion."\" xmlns:atom=\"http://www.w3.org/2005/Atom\">\n"; - $feed.= " \n"; - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars($this->title),100)."\n"; - $this->descriptionTruncSize = 500; - $feed.= " ".$this->getDescription()."\n"; - $feed.= " ".$this->link."\n"; - $feed.= " syndicationURL."\" rel=\"self\" type=\"application/rss+xml\" />\n"; - $now = new FeedDate(); - $feed.= " ".htmlspecialchars($now->rfc822())."\n"; - $feed.= " ".FEEDCREATOR_VERSION."\n"; - - if ($this->image!=null) { - $feed.= " \n"; - $feed.= " ".$this->image->url."\n"; - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars($this->image->title),100)."\n"; - $feed.= " ".$this->image->link."\n"; - if ($this->image->width!="") { - $feed.= " ".$this->image->width."\n"; - } - if ($this->image->height!="") { - $feed.= " ".$this->image->height."\n"; - } - if ($this->image->description!="") { - $feed.= " ".$this->image->getDescription()."\n"; - } - $feed.= " \n"; - } - if ($this->language!="") { - $feed.= " ".$this->language."\n"; - } - if ($this->copyright!="") { - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars($this->copyright),100)."\n"; - } - if ($this->editor!="") { - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars($this->editor),100)."\n"; - } - if ($this->webmaster!="") { - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars($this->webmaster),100)."\n"; - } - if ($this->pubDate!="") { - $pubDate = new FeedDate($this->pubDate); - $feed.= " ".htmlspecialchars($pubDate->rfc822())."\n"; - } - if ($this->category!="") { - $feed.= " ".htmlspecialchars($this->category)."\n"; - } - if ($this->docs!="") { - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars($this->docs),500)."\n"; - } - if ($this->ttl!="") { - $feed.= " ".htmlspecialchars($this->ttl)."\n"; - } - if ($this->rating!="") { - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars($this->rating),500)."\n"; - } - if ($this->skipHours!="") { - $feed.= " ".htmlspecialchars($this->skipHours)."\n"; - } - if ($this->skipDays!="") { - $feed.= " ".htmlspecialchars($this->skipDays)."\n"; - } - $feed.= $this->_createAdditionalElements($this->additionalElements, " "); - - for ($i=0;$iitems);$i++) { - $feed.= " \n"; - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars(strip_tags($this->items[$i]->title)),100)."\n"; - $feed.= " ".htmlspecialchars($this->items[$i]->link)."\n"; - $feed.= " ".$this->items[$i]->getDescription()."\n"; - - if ($this->items[$i]->author!="") { - $feed.= " ".htmlspecialchars($this->items[$i]->author)."\n"; - } - /* - // on hold - if ($this->items[$i]->source!="") { - $feed.= " ".htmlspecialchars($this->items[$i]->source)."\n"; - } - */ - if ($this->items[$i]->category!="") { - $feed.= " ".htmlspecialchars($this->items[$i]->category)."\n"; - } - if ($this->items[$i]->comments!="") { - $feed.= " ".htmlspecialchars($this->items[$i]->comments)."\n"; - } - if ($this->items[$i]->date!="") { - $itemDate = new FeedDate($this->items[$i]->date); - $feed.= " ".htmlspecialchars($itemDate->rfc822())."\n"; - } - if ($this->items[$i]->guid!="") { - $feed.= " items[$i]->guidIsPermaLink == false) { - $feed.= " isPermaLink=\"false\""; - } - $feed.= ">".htmlspecialchars($this->items[$i]->guid)."\n"; - } - $feed.= $this->_createAdditionalElements($this->items[$i]->additionalElements, " "); - $feed.= " \n"; - } - $feed.= " \n"; - $feed.= "\n"; - return $feed; - } -} - - - -/** - * RSSCreator20 is a FeedCreator that implements RDF Site Summary (RSS) 2.0. - * - * @see http://backend.userland.com/rss - * @since 1.3 - * @author Kai Blankenhorn - */ -class RSSCreator20 extends RSSCreator091 { - - function RSSCreator20() { - parent::_setRSSVersion("2.0"); - } - -} - - -/** - * PIECreator01 is a FeedCreator that implements the emerging PIE specification, - * as in http://intertwingly.net/wiki/pie/Syntax. - * - * @deprecated - * @since 1.3 - * @author Scott Reynen and Kai Blankenhorn - */ -class PIECreator01 extends FeedCreator { - - function PIECreator01() { - $this->encoding = "utf-8"; - } - - function createFeed() { - $feed = "encoding."\"?>\n"; - $feed.= $this->_createStylesheetReferences(); - $feed.= "\n"; - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars($this->title),100)."\n"; - $this->truncSize = 500; - $feed.= " ".$this->getDescription()."\n"; - $feed.= " ".$this->link."\n"; - for ($i=0;$iitems);$i++) { - $feed.= " \n"; - $feed.= " ".FeedCreator::iTrunc(htmlspecialchars(strip_tags($this->items[$i]->title)),100)."\n"; - $feed.= " ".htmlspecialchars($this->items[$i]->link)."\n"; - $itemDate = new FeedDate($this->items[$i]->date); - $feed.= " ".htmlspecialchars($itemDate->iso8601())."\n"; - $feed.= " ".htmlspecialchars($itemDate->iso8601())."\n"; - $feed.= " ".htmlspecialchars($itemDate->iso8601())."\n"; - $feed.= " ".htmlspecialchars($this->items[$i]->guid)."\n"; - if ($this->items[$i]->author!="") { - $feed.= " \n"; - $feed.= " ".htmlspecialchars($this->items[$i]->author)."\n"; - if ($this->items[$i]->authorEmail!="") { - $feed.= " ".$this->items[$i]->authorEmail."\n"; - } - $feed.=" \n"; - } - $feed.= " \n"; - $feed.= "
    ".$this->items[$i]->getDescription()."
    \n"; - $feed.= "
    \n"; - $feed.= "
    \n"; - } - $feed.= "
    \n"; - return $feed; - } -} - - -/** - * AtomCreator03 is a FeedCreator that implements the atom specification, - * as in http://www.intertwingly.net/wiki/pie/FrontPage. - * Please note that just by using AtomCreator03 you won't automatically - * produce valid atom files. For example, you have to specify either an editor - * for the feed or an author for every single feed item. - * - * Some elements have not been implemented yet. These are (incomplete list): - * author URL, item author's email and URL, item contents, alternate links, - * other link content types than text/html. Some of them may be created with - * AtomCreator03::additionalElements. - * - * @see FeedCreator#additionalElements - * @since 1.6 - * @author Kai Blankenhorn , Scott Reynen - */ -class AtomCreator03 extends FeedCreator { - - function AtomCreator03() { - $this->contentType = "application/atom+xml"; - $this->encoding = "utf-8"; - } - - function createFeed() { - $feed = "encoding."\"?>\n"; - $feed.= $this->_createGeneratorComment(); - $feed.= $this->_createStylesheetReferences(); - $feed.= "language!="") { - $feed.= " xml:lang=\"".$this->language."\""; - } - $feed.= ">\n"; - $feed.= " ".htmlspecialchars($this->title)."\n"; - $feed.= " ".htmlspecialchars($this->description)."\n"; - $feed.= " link)."\"/>\n"; - $feed.= " ".htmlspecialchars($this->link)."\n"; - $now = new FeedDate(); - $feed.= " ".htmlspecialchars($now->iso8601())."\n"; - if ($this->editor!="") { - $feed.= " \n"; - $feed.= " ".$this->editor."\n"; - if ($this->editorEmail!="") { - $feed.= " ".$this->editorEmail."\n"; - } - $feed.= " \n"; - } - $feed.= " ".FEEDCREATOR_VERSION."\n"; - $feed.= $this->_createAdditionalElements($this->additionalElements, " "); - for ($i=0;$iitems);$i++) { - $feed.= " \n"; - $feed.= " ".htmlspecialchars(strip_tags($this->items[$i]->title))."\n"; - $feed.= " items[$i]->link)."\"/>\n"; - if ($this->items[$i]->date=="") { - $this->items[$i]->date = time(); - } - $itemDate = new FeedDate($this->items[$i]->date); - $feed.= " ".htmlspecialchars($itemDate->iso8601())."\n"; - $feed.= " ".htmlspecialchars($itemDate->iso8601())."\n"; - $feed.= " ".htmlspecialchars($itemDate->iso8601())."\n"; - $feed.= " ".htmlspecialchars($this->items[$i]->link)."\n"; - $feed.= $this->_createAdditionalElements($this->items[$i]->additionalElements, " "); - if ($this->items[$i]->author!="") { - $feed.= " \n"; - $feed.= " ".htmlspecialchars($this->items[$i]->author)."\n"; - $feed.= " \n"; - } - if ($this->items[$i]->description!="") { - $feed.= " ".htmlspecialchars($this->items[$i]->description)."\n"; - } - $feed.= " \n"; - } - $feed.= "\n"; - return $feed; - } -} - - -/** - * MBOXCreator is a FeedCreator that implements the mbox format - * as described in http://www.qmail.org/man/man5/mbox.html - * - * @since 1.3 - * @author Kai Blankenhorn - */ -class MBOXCreator extends FeedCreator { - - function MBOXCreator() { - $this->contentType = "text/plain"; - $this->encoding = "ISO-8859-15"; - } - - function qp_enc($input = "", $line_max = 76) { - $hex = array('0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'); - $lines = preg_split("/(?:\r\n|\r|\n)/", $input); - $eol = "\r\n"; - $escape = "="; - $output = ""; - while( list(, $line) = each($lines) ) { - //$line = rtrim($line); // remove trailing white space -> no =20\r\n necessary - $linlen = strlen($line); - $newline = ""; - for($i = 0; $i < $linlen; $i++) { - $c = substr($line, $i, 1); - $dec = ord($c); - if ( ($dec == 32) && ($i == ($linlen - 1)) ) { // convert space at eol only - $c = "=20"; - } elseif ( ($dec == 61) || ($dec < 32 ) || ($dec > 126) ) { // always encode "\t", which is *not* required - $h2 = floor($dec/16); $h1 = floor($dec%16); - $c = $escape.$hex["$h2"].$hex["$h1"]; - } - if ( (strlen($newline) + strlen($c)) >= $line_max ) { // CRLF is not counted - $output .= $newline.$escape.$eol; // soft line break; " =\r\n" is okay - $newline = ""; - } - $newline .= $c; - } // end of for - $output .= $newline.$eol; - } - return trim($output); - } - - - /** - * Builds the MBOX contents. - * @return string the feed's complete text - */ - function createFeed() { - for ($i=0;$iitems);$i++) { - if ($this->items[$i]->author!="") { - $from = $this->items[$i]->author; - } else { - $from = $this->title; - } - $itemDate = new FeedDate($this->items[$i]->date); - $feed.= "From ".strtr(MBOXCreator::qp_enc($from)," ","_")." ".date("D M d H:i:s Y",$itemDate->unix())."\n"; - $feed.= "Content-Type: text/plain;\n"; - $feed.= " charset=\"".$this->encoding."\"\n"; - $feed.= "Content-Transfer-Encoding: quoted-printable\n"; - $feed.= "Content-Type: text/plain\n"; - $feed.= "From: \"".MBOXCreator::qp_enc($from)."\"\n"; - $feed.= "Date: ".$itemDate->rfc822()."\n"; - $feed.= "Subject: ".MBOXCreator::qp_enc(FeedCreator::iTrunc($this->items[$i]->title,100))."\n"; - $feed.= "\n"; - $body = chunk_split(MBOXCreator::qp_enc($this->items[$i]->description)); - $feed.= preg_replace("~\nFrom ([^\n]*)(\n?)~","\n>From $1$2\n",$body); - $feed.= "\n"; - $feed.= "\n"; - } - return $feed; - } - - /** - * Generate a filename for the feed cache file. Overridden from FeedCreator to prevent XML data types. - * @return string the feed cache filename - * @since 1.4 - * @access private - */ - function _generateFilename() { - $fileInfo = pathinfo($_SERVER["PHP_SELF"]); - return substr($fileInfo["basename"],0,-(strlen($fileInfo["extension"])+1)).".mbox"; - } -} - - -/** - * OPMLCreator is a FeedCreator that implements OPML 1.0. - * - * @see http://opml.scripting.com/spec - * @author Dirk Clemens, Kai Blankenhorn - * @since 1.5 - */ -class OPMLCreator extends FeedCreator { - - function OPMLCreator() { - $this->encoding = "utf-8"; - } - - function createFeed() { - $feed = "encoding."\"?>\n"; - $feed.= $this->_createGeneratorComment(); - $feed.= $this->_createStylesheetReferences(); - $feed.= "\n"; - $feed.= " \n"; - $feed.= " ".htmlspecialchars($this->title)."\n"; - if ($this->pubDate!="") { - $date = new FeedDate($this->pubDate); - $feed.= " ".$date->rfc822()."\n"; - } - if ($this->lastBuildDate!="") { - $date = new FeedDate($this->lastBuildDate); - $feed.= " ".$date->rfc822()."\n"; - } - if ($this->editor!="") { - $feed.= " ".$this->editor."\n"; - } - if ($this->editorEmail!="") { - $feed.= " ".$this->editorEmail."\n"; - } - $feed.= " \n"; - $feed.= " \n"; - for ($i=0;$iitems);$i++) { - $feed.= " items[$i]->title,"\n\r"," "))); - $feed.= " title=\"".$title."\""; - $feed.= " text=\"".$title."\""; - //$feed.= " description=\"".htmlspecialchars($this->items[$i]->description)."\""; - $feed.= " url=\"".htmlspecialchars($this->items[$i]->link)."\""; - $feed.= "/>\n"; - } - $feed.= " \n"; - $feed.= "\n"; - return $feed; - } -} - - - -/** - * HTMLCreator is a FeedCreator that writes an HTML feed file to a specific - * location, overriding the createFeed method of the parent FeedCreator. - * The HTML produced can be included over http by scripting languages, or serve - * as the source for an IFrame. - * All output by this class is embedded in
    tags to enable formatting - * using CSS. - * - * @author Pascal Van Hecke - * @since 1.7 - */ -class HTMLCreator extends FeedCreator { - - var $contentType = "text/html"; - - /** - * Contains HTML to be output at the start of the feed's html representation. - */ - var $header; - - /** - * Contains HTML to be output at the end of the feed's html representation. - */ - var $footer ; - - /** - * Contains HTML to be output between entries. A separator is only used in - * case of multiple entries. - */ - var $separator; - - /** - * Used to prefix the stylenames to make sure they are unique - * and do not clash with stylenames on the users' page. - */ - var $stylePrefix; - - /** - * Determines whether the links open in a new window or not. - */ - var $openInNewWindow = true; - - var $imageAlign ="right"; - - /** - * In case of very simple output you may want to get rid of the style tags, - * hence this variable. There's no equivalent on item level, but of course you can - * add strings to it while iterating over the items ($this->stylelessOutput .= ...) - * and when it is non-empty, ONLY the styleless output is printed, the rest is ignored - * in the function createFeed(). - */ - var $stylelessOutput =""; - - /** - * Writes the HTML. - * @return string the scripts's complete text - */ - function createFeed() { - // if there is styleless output, use the content of this variable and ignore the rest - if ($this->stylelessOutput!="") { - return $this->stylelessOutput; - } - - //if no stylePrefix is set, generate it yourself depending on the script name - if ($this->stylePrefix=="") { - $this->stylePrefix = str_replace(".", "_", $this->_generateFilename())."_"; - } - - //set an openInNewWindow_token_to be inserted or not - if ($this->openInNewWindow) { - $targetInsert = " target='_blank'"; - } - - // use this array to put the lines in and implode later with "document.write" javascript - $feedArray = array(); - if ($this->image!=null) { - $imageStr = "". - "".
-							FeedCreator::iTrunc(htmlspecialchars($this->image->title),100).
-							"image->width) { - $imageStr .=" width='".$this->image->width. "' "; - } - if ($this->image->height) { - $imageStr .=" height='".$this->image->height."' "; - } - $imageStr .="/>"; - $feedArray[] = $imageStr; - } - - if ($this->title) { - $feedArray[] = ""; - } - if ($this->getDescription()) { - $feedArray[] = "
    ". - str_replace("]]>", "", str_replace("getDescription())). - "
    "; - } - - if ($this->header) { - $feedArray[] = "
    ".$this->header."
    "; - } - - for ($i=0;$iitems);$i++) { - if ($this->separator and $i > 0) { - $feedArray[] = "
    ".$this->separator."
    "; - } - - if ($this->items[$i]->title) { - if ($this->items[$i]->link) { - $feedArray[] = - ""; - } else { - $feedArray[] = - "
    ". - FeedCreator::iTrunc(htmlspecialchars(strip_tags($this->items[$i]->title)),100). - "
    "; - } - } - if ($this->items[$i]->getDescription()) { - $feedArray[] = - "
    ". - str_replace("]]>", "", str_replace("items[$i]->getDescription())). - "
    "; - } - } - if ($this->footer) { - $feedArray[] = "
    ".$this->footer."
    "; - } - - $feed= "".join($feedArray, "\r\n"); - return $feed; - } - - /** - * Overrrides parent to produce .html extensions - * - * @return string the feed cache filename - * @since 1.4 - * @access private - */ - function _generateFilename() { - $fileInfo = pathinfo($_SERVER["PHP_SELF"]); - return substr($fileInfo["basename"],0,-(strlen($fileInfo["extension"])+1)).".html"; - } -} - - -/** - * JSCreator is a class that writes a js file to a specific - * location, overriding the createFeed method of the parent HTMLCreator. - * - * @author Pascal Van Hecke - */ -class JSCreator extends HTMLCreator { - var $contentType = "text/javascript"; - - /** - * writes the javascript - * @return string the scripts's complete text - */ - function createFeed() - { - $feed = parent::createFeed(); - $feedArray = explode("\n",$feed); - - $jsFeed = ""; - foreach ($feedArray as $value) { - $jsFeed .= "document.write('".trim(addslashes($value))."');\n"; - } - return $jsFeed; - } - - /** - * Overrrides parent to produce .js extensions - * - * @return string the feed cache filename - * @since 1.4 - * @access private - */ - function _generateFilename() { - $fileInfo = pathinfo($_SERVER["PHP_SELF"]); - return substr($fileInfo["basename"],0,-(strlen($fileInfo["extension"])+1)).".js"; - } - -} - - - -/*** TEST SCRIPT ********************************************************* - -//include("feedcreator.class.php"); - -$rss = new UniversalFeedCreator(); -$rss->useCached(); -$rss->title = "PHP news"; -$rss->description = "daily news from the PHP scripting world"; - -//optional -//$rss->descriptionTruncSize = 500; -//$rss->descriptionHtmlSyndicated = true; -//$rss->xslStyleSheet = "http://feedster.com/rss20.xsl"; - -$rss->link = "http://www.dailyphp.net/news"; -$rss->feedURL = "http://www.dailyphp.net/".$PHP_SELF; - -$image = new FeedImage(); -$image->title = "dailyphp.net logo"; -$image->url = "http://www.dailyphp.net/images/logo.gif"; -$image->link = "http://www.dailyphp.net"; -$image->description = "Feed provided by dailyphp.net. Click to visit."; - -//optional -$image->descriptionTruncSize = 500; -$image->descriptionHtmlSyndicated = true; - -$rss->image = $image; - -// get your news items from somewhere, e.g. your database: -//mysql_select_db($dbHost, $dbUser, $dbPass); -//$res = mysql_query("SELECT * FROM news ORDER BY newsdate DESC"); -//while ($data = mysql_fetch_object($res)) { - $item = new FeedItem(); - $item->title = "This is an the test title of an item"; - $item->link = "http://localhost/item/"; - $item->description = "description in
    HTML"; - - //optional - //item->descriptionTruncSize = 500; - $item->descriptionHtmlSyndicated = true; - - $item->date = time(); - $item->source = "http://www.dailyphp.net"; - $item->author = "John Doe"; - - $rss->addItem($item); -//} - -// valid format strings are: RSS0.91, RSS1.0, RSS2.0, PIE0.1, MBOX, OPML, ATOM0.3, HTML, JS -echo $rss->saveFeed("RSS0.91", "feed.xml"); - - - -***************************************************************************/ - -?> diff --git a/web/lib/gettext.php b/web/lib/gettext.php deleted file mode 100644 index 098f0e5e..00000000 --- a/web/lib/gettext.php +++ /dev/null @@ -1,432 +0,0 @@ -. - Copyright (c) 2005 Nico Kaiser - - This file is part of PHP-gettext. - - PHP-gettext is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - PHP-gettext is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with PHP-gettext; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -*/ - -/** - * Provides a simple gettext replacement that works independently from - * the system's gettext abilities. - * It can read MO files and use them for translating strings. - * The files are passed to gettext_reader as a Stream (see streams.php) - * - * This version has the ability to cache all strings and translations to - * speed up the string lookup. - * While the cache is enabled by default, it can be switched off with the - * second parameter in the constructor (e.g. whenusing very large MO files - * that you don't want to keep in memory) - */ -class gettext_reader { - //public: - var $error = 0; // public variable that holds error code (0 if no error) - - //private: - var $BYTEORDER = 0; // 0: low endian, 1: big endian - var $STREAM = NULL; - var $short_circuit = false; - var $enable_cache = false; - var $originals = NULL; // offset of original table - var $translations = NULL; // offset of translation table - var $pluralheader = NULL; // cache header field for plural forms - var $total = 0; // total string count - var $table_originals = NULL; // table for original strings (offsets) - var $table_translations = NULL; // table for translated strings (offsets) - var $cache_translations = NULL; // original -> translation mapping - - - /* Methods */ - - - /** - * Reads a 32bit Integer from the Stream - * - * @access private - * @return Integer from the Stream - */ - function readint() { - if ($this->BYTEORDER == 0) { - // low endian - $input=unpack('V', $this->STREAM->read(4)); - return array_shift($input); - } else { - // big endian - $input=unpack('N', $this->STREAM->read(4)); - return array_shift($input); - } - } - - function read($bytes) { - return $this->STREAM->read($bytes); - } - - /** - * Reads an array of Integers from the Stream - * - * @param int count How many elements should be read - * @return Array of Integers - */ - function readintarray($count) { - if ($this->BYTEORDER == 0) { - // low endian - return unpack('V'.$count, $this->STREAM->read(4 * $count)); - } else { - // big endian - return unpack('N'.$count, $this->STREAM->read(4 * $count)); - } - } - - /** - * Constructor - * - * @param object Reader the StreamReader object - * @param boolean enable_cache Enable or disable caching of strings (default on) - */ - function __construct($Reader, $enable_cache = true) { - // If there isn't a StreamReader, turn on short circuit mode. - if (! $Reader || isset($Reader->error) ) { - $this->short_circuit = true; - return; - } - - // Caching can be turned off - $this->enable_cache = $enable_cache; - - $MAGIC1 = "\x95\x04\x12\xde"; - $MAGIC2 = "\xde\x12\x04\x95"; - - $this->STREAM = $Reader; - $magic = $this->read(4); - if ($magic == $MAGIC1) { - $this->BYTEORDER = 1; - } elseif ($magic == $MAGIC2) { - $this->BYTEORDER = 0; - } else { - $this->error = 1; // not MO file - return false; - } - - // FIXME: Do we care about revision? We should. - $revision = $this->readint(); - - $this->total = $this->readint(); - $this->originals = $this->readint(); - $this->translations = $this->readint(); - } - - /** - * Loads the translation tables from the MO file into the cache - * If caching is enabled, also loads all strings into a cache - * to speed up translation lookups - * - * @access private - */ - function load_tables() { - if (is_array($this->cache_translations) && - is_array($this->table_originals) && - is_array($this->table_translations)) - return; - - /* get original and translations tables */ - if (!is_array($this->table_originals)) { - $this->STREAM->seekto($this->originals); - $this->table_originals = $this->readintarray($this->total * 2); - } - if (!is_array($this->table_translations)) { - $this->STREAM->seekto($this->translations); - $this->table_translations = $this->readintarray($this->total * 2); - } - - if ($this->enable_cache) { - $this->cache_translations = array (); - /* read all strings in the cache */ - for ($i = 0; $i < $this->total; $i++) { - $this->STREAM->seekto($this->table_originals[$i * 2 + 2]); - $original = $this->STREAM->read($this->table_originals[$i * 2 + 1]); - $this->STREAM->seekto($this->table_translations[$i * 2 + 2]); - $translation = $this->STREAM->read($this->table_translations[$i * 2 + 1]); - $this->cache_translations[$original] = $translation; - } - } - } - - /** - * Returns a string from the "originals" table - * - * @access private - * @param int num Offset number of original string - * @return string Requested string if found, otherwise '' - */ - function get_original_string($num) { - $length = $this->table_originals[$num * 2 + 1]; - $offset = $this->table_originals[$num * 2 + 2]; - if (! $length) - return ''; - $this->STREAM->seekto($offset); - $data = $this->STREAM->read($length); - return (string)$data; - } - - /** - * Returns a string from the "translations" table - * - * @access private - * @param int num Offset number of original string - * @return string Requested string if found, otherwise '' - */ - function get_translation_string($num) { - $length = $this->table_translations[$num * 2 + 1]; - $offset = $this->table_translations[$num * 2 + 2]; - if (! $length) - return ''; - $this->STREAM->seekto($offset); - $data = $this->STREAM->read($length); - return (string)$data; - } - - /** - * Binary search for string - * - * @access private - * @param string string - * @param int start (internally used in recursive function) - * @param int end (internally used in recursive function) - * @return int string number (offset in originals table) - */ - function find_string($string, $start = -1, $end = -1) { - if (($start == -1) or ($end == -1)) { - // find_string is called with only one parameter, set start end end - $start = 0; - $end = $this->total; - } - if (abs($start - $end) <= 1) { - // We're done, now we either found the string, or it doesn't exist - $txt = $this->get_original_string($start); - if ($string == $txt) - return $start; - else - return -1; - } else if ($start > $end) { - // start > end -> turn around and start over - return $this->find_string($string, $end, $start); - } else { - // Divide table in two parts - $half = (int)(($start + $end) / 2); - $cmp = strcmp($string, $this->get_original_string($half)); - if ($cmp == 0) - // string is exactly in the middle => return it - return $half; - else if ($cmp < 0) - // The string is in the upper half - return $this->find_string($string, $start, $half); - else - // The string is in the lower half - return $this->find_string($string, $half, $end); - } - } - - /** - * Translates a string - * - * @access public - * @param string string to be translated - * @return string translated string (or original, if not found) - */ - function translate($string) { - if ($this->short_circuit) - return $string; - $this->load_tables(); - - if ($this->enable_cache) { - // Caching enabled, get translated string from cache - if (array_key_exists($string, $this->cache_translations)) - return $this->cache_translations[$string]; - else - return $string; - } else { - // Caching not enabled, try to find string - $num = $this->find_string($string); - if ($num == -1) - return $string; - else - return $this->get_translation_string($num); - } - } - - /** - * Sanitize plural form expression for use in PHP eval call. - * - * @access private - * @return string sanitized plural form expression - */ - function sanitize_plural_expression($expr) { - // Get rid of disallowed characters. - $expr = preg_replace('@[^a-zA-Z0-9_:;\(\)\?\|\&=!<>+*/\%-]@', '', $expr); - - // Add parenthesis for tertiary '?' operator. - $expr .= ';'; - $res = ''; - $p = 0; - for ($i = 0; $i < strlen($expr); $i++) { - $ch = $expr[$i]; - switch ($ch) { - case '?': - $res .= ' ? ('; - $p++; - break; - case ':': - $res .= ') : ('; - break; - case ';': - $res .= str_repeat( ')', $p) . ';'; - $p = 0; - break; - default: - $res .= $ch; - } - } - return $res; - } - - /** - * Parse full PO header and extract only plural forms line. - * - * @access private - * @return string verbatim plural form header field - */ - function extract_plural_forms_header_from_po_header($header) { - if (preg_match("/(^|\n)plural-forms: ([^\n]*)\n/i", $header, $regs)) - $expr = $regs[2]; - else - $expr = "nplurals=2; plural=n == 1 ? 0 : 1;"; - return $expr; - } - - /** - * Get possible plural forms from MO header - * - * @access private - * @return string plural form header - */ - function get_plural_forms() { - // lets assume message number 0 is header - // this is true, right? - $this->load_tables(); - - // cache header field for plural forms - if (! is_string($this->pluralheader)) { - if ($this->enable_cache) { - $header = $this->cache_translations[""]; - } else { - $header = $this->get_translation_string(0); - } - $expr = $this->extract_plural_forms_header_from_po_header($header); - $this->pluralheader = $this->sanitize_plural_expression($expr); - } - return $this->pluralheader; - } - - /** - * Detects which plural form to take - * - * @access private - * @param n count - * @return int array index of the right plural form - */ - function select_string($n) { - $string = $this->get_plural_forms(); - $string = str_replace('nplurals',"\$total",$string); - $string = str_replace("n",$n,$string); - $string = str_replace('plural',"\$plural",$string); - - $total = 0; - $plural = 0; - - eval("$string"); - if ($plural >= $total) $plural = $total - 1; - return $plural; - } - - /** - * Plural version of gettext - * - * @access public - * @param string single - * @param string plural - * @param string number - * @return translated plural form - */ - function ngettext($single, $plural, $number) { - if ($this->short_circuit) { - if ($number != 1) - return $plural; - else - return $single; - } - - // find out the appropriate form - $select = $this->select_string($number); - - // this should contains all strings separated by NULLs - $key = $single . chr(0) . $plural; - - - if ($this->enable_cache) { - if (! array_key_exists($key, $this->cache_translations)) { - return ($number != 1) ? $plural : $single; - } else { - $result = $this->cache_translations[$key]; - $list = explode(chr(0), $result); - return $list[$select]; - } - } else { - $num = $this->find_string($key); - if ($num == -1) { - return ($number != 1) ? $plural : $single; - } else { - $result = $this->get_translation_string($num); - $list = explode(chr(0), $result); - return $list[$select]; - } - } - } - - function pgettext($context, $msgid) { - $key = $context . chr(4) . $msgid; - $ret = $this->translate($key); - if (strpos($ret, "\004") !== false) { - return $msgid; - } else { - return $ret; - } - } - - function npgettext($context, $singular, $plural, $number) { - $key = $context . chr(4) . $singular; - $ret = $this->ngettext($key, $plural, $number); - if (strpos($ret, "\004") !== false) { - return $singular; - } else { - return $ret; - } - - } -} - -?> diff --git a/web/lib/pkgbasefuncs.inc.php b/web/lib/pkgbasefuncs.inc.php deleted file mode 100644 index a053962e..00000000 --- a/web/lib/pkgbasefuncs.inc.php +++ /dev/null @@ -1,1253 +0,0 @@ -query($q); - if (!$result) { - return null; - } - - return $result->fetchColumn(0); -} - -/** - * Get all package comment information for a specific package base - * - * @param int $base_id The package base ID to get comments for - * @param int $limit Maximum number of comments to return (0 means unlimited) - * @param bool $include_deleted True if deleted comments should be included - * @param bool $only_pinned True when only pinned comments are to be included - * - * @return array All package comment information for a specific package base - */ -function pkgbase_comments($base_id, $limit, $include_deleted, $only_pinned=false, $offset=0) { - $base_id = intval($base_id); - $limit = intval($limit); - if (!$base_id) { - return null; - } - - $dbh = DB::connect(); - $q = "SELECT PackageComments.ID, A.UserName AS UserName, UsersID, Comments, "; - $q.= "PackageBaseID, CommentTS, DelTS, EditedTS, B.UserName AS EditUserName, "; - $q.= "DelUsersID, C.UserName AS DelUserName, RenderedComment, "; - $q.= "PinnedTS FROM PackageComments "; - $q.= "LEFT JOIN Users A ON PackageComments.UsersID = A.ID "; - $q.= "LEFT JOIN Users B ON PackageComments.EditedUsersID = B.ID "; - $q.= "LEFT JOIN Users C ON PackageComments.DelUsersID = C.ID "; - $q.= "WHERE PackageBaseID = " . $base_id . " "; - - if (!$include_deleted) { - $q.= "AND DelTS IS NULL "; - } - if ($only_pinned) { - $q.= "AND NOT PinnedTS = 0 "; - } - $q.= "ORDER BY CommentTS DESC"; - if ($limit > 0) { - $q.=" LIMIT " . $limit; - } - if ($offset > 0) { - $q.=" OFFSET " . $offset; - } - $result = $dbh->query($q); - if (!$result) { - return null; - } - - return $result->fetchAll(); -} - -/* - * Invoke the comment rendering script. - * - * @param int $id ID of the comment to render - * - * @return void - */ -function render_comment($id) { - $cmd = config_get('options', 'render-comment-cmd'); - $cmd .= ' ' . intval($id); - - $descspec = array( - 0 => array('pipe', 'r'), - 1 => array('pipe', 'w'), - ); - - $p = proc_open($cmd, $descspec, $pipes); - - if (!is_resource($p)) { - return false; - } - - fclose($pipes[0]); - fclose($pipes[1]); - - return proc_close($p); -} - -/** - * Add a comment to a package page and send out appropriate notifications - * - * @param string $base_id The package base ID to add the comment on - * @param string $uid The user ID of the individual who left the comment - * @param string $comment The comment left on a package page - * - * @return void - */ -function pkgbase_add_comment($base_id, $uid, $comment) { - $dbh = DB::connect(); - - if (trim($comment) == '') { - return array(false, __('Comment cannot be empty.')); - } - - $q = "INSERT INTO PackageComments "; - $q.= "(PackageBaseID, UsersID, Comments, RenderedComment, CommentTS) "; - $q.= "VALUES (" . intval($base_id) . ", " . $uid . ", "; - $q.= $dbh->quote($comment) . ", '', " . strval(time()) . ")"; - $dbh->exec($q); - $comment_id = $dbh->lastInsertId(); - - render_comment($comment_id); - - notify(array('comment', $uid, $base_id, $comment_id)); - - return array(true, __('Comment has been added.')); -} - -/** - * Pin/unpin a package comment - * - * @param bool $unpin True if unpinning rather than pinning - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_pin_comment($unpin=false) { - $uid = uid_from_sid($_COOKIE["AURSID"]); - - if (!$uid) { - return array(false, __("You must be logged in before you can edit package information.")); - } - - if (isset($_POST["comment_id"])) { - $comment_id = $_POST["comment_id"]; - } else { - return array(false, __("Missing comment ID.")); - } - - if (!$unpin) { - if (pkgbase_comments_count($_POST['package_base'], false, true) >= 5){ - return array(false, __("No more than 5 comments can be pinned.")); - } - } - - if (!can_pin_comment($comment_id)) { - if (!$unpin) { - return array(false, __("You are not allowed to pin this comment.")); - } else { - return array(false, __("You are not allowed to unpin this comment.")); - } - } - - $dbh = DB::connect(); - $q = "UPDATE PackageComments "; - if (!$unpin) { - $q.= "SET PinnedTS = " . strval(time()) . " "; - } else { - $q.= "SET PinnedTS = 0 "; - } - $q.= "WHERE ID = " . intval($comment_id); - $dbh->exec($q); - - if (!$unpin) { - return array(true, __("Comment has been pinned.")); - } else { - return array(true, __("Comment has been unpinned.")); - } -} - -/** - - * Get a list of all packages a logged-in user has voted for - * - * @param string $sid The session ID of the visitor - * - * @return array All packages the visitor has voted for - */ -function pkgbase_votes_from_sid($sid="") { - $pkgs = array(); - if (!$sid) {return $pkgs;} - $dbh = DB::connect(); - $q = "SELECT PackageBaseID "; - $q.= "FROM PackageVotes, Users, Sessions "; - $q.= "WHERE Users.ID = Sessions.UsersID "; - $q.= "AND Users.ID = PackageVotes.UsersID "; - $q.= "AND Sessions.SessionID = " . $dbh->quote($sid); - $result = $dbh->query($q); - if ($result) { - while ($row = $result->fetch(PDO::FETCH_NUM)) { - $pkgs[$row[0]] = 1; - } - } - return $pkgs; -} - -/** - * Get the package base details - * - * @param string $id The package base ID to get description for - * - * @return array The package base's details OR error message - **/ -function pkgbase_get_details($base_id) { - $dbh = DB::connect(); - - $q = "SELECT PackageBases.ID, PackageBases.Name, "; - $q.= "PackageBases.NumVotes, PackageBases.Popularity, "; - $q.= "PackageBases.OutOfDateTS, PackageBases.SubmittedTS, "; - $q.= "PackageBases.ModifiedTS, PackageBases.SubmitterUID, "; - $q.= "PackageBases.MaintainerUID, PackageBases.PackagerUID, "; - $q.= "PackageBases.FlaggerUID, "; - $q.= "(SELECT COUNT(*) FROM PackageRequests "; - $q.= " WHERE PackageRequests.PackageBaseID = PackageBases.ID "; - $q.= " AND PackageRequests.Status = 0) AS RequestCount "; - $q.= "FROM PackageBases "; - $q.= "WHERE PackageBases.ID = " . intval($base_id); - $result = $dbh->query($q); - - $row = array(); - - if (!$result) { - $row['error'] = __("Error retrieving package details."); - } - else { - $row = $result->fetch(PDO::FETCH_ASSOC); - if (empty($row)) { - $row['error'] = __("Package details could not be found."); - } - } - - return $row; -} - -/** - * Display the package base details page - * - * @param string $id The package base ID to get details page for - * @param array $row Package base details retrieved by pkgbase_get_details() - * @param string $SID The session ID of the visitor - * - * @return void - */ -function pkgbase_display_details($base_id, $row, $SID="") { - if (isset($row['error'])) { - print "

    " . $row['error'] . "

    \n"; - } - else { - $pkgbase_name = pkgbase_name_from_id($base_id); - - include('pkgbase_details.php'); - - if ($SID) { - $comment_section = "package"; - include('pkg_comment_box.php'); - } - - $include_deleted = has_credential(CRED_COMMENT_VIEW_DELETED); - - $limit_pinned = isset($_GET['pinned']) ? 0 : 5; - $pinned = pkgbase_comments($base_id, $limit_pinned, false, true); - if (!empty($pinned)) { - $comment_section = "package"; - include('pkg_comments.php'); - } - unset($pinned); - - - $total_comment_count = pkgbase_comments_count($base_id, $include_deleted); - list($pagination_templs, $per_page, $offset) = calculate_pagination($total_comment_count); - - $comments = pkgbase_comments($base_id, $per_page, $include_deleted, false, $offset); - if (!empty($comments)) { - $comment_section = "package"; - include('pkg_comments.php'); - } - } -} - -/** - * Convert a list of package IDs into a list of corresponding package bases. - * - * @param array|int $ids Array of package IDs to convert - * - * @return array|int List of package base IDs - */ -function pkgbase_from_pkgid($ids) { - $dbh = DB::connect(); - - if (is_array($ids)) { - $q = "SELECT PackageBaseID FROM Packages "; - $q.= "WHERE ID IN (" . implode(",", $ids) . ")"; - $result = $dbh->query($q); - return $result->fetchAll(PDO::FETCH_COLUMN, 0); - } else { - $q = "SELECT PackageBaseID FROM Packages "; - $q.= "WHERE ID = " . $ids; - $result = $dbh->query($q); - return $result->fetch(PDO::FETCH_COLUMN, 0); - } -} - -/** - * Retrieve ID of a package base by name - * - * @param string $name The package base name to retrieve the ID for - * - * @return int The ID of the package base - */ -function pkgbase_from_name($name) { - $dbh = DB::connect(); - $q = "SELECT ID FROM PackageBases WHERE Name = " . $dbh->quote($name); - $result = $dbh->query($q); - return $result->fetch(PDO::FETCH_COLUMN, 0); -} - -/** - * Retrieve the name of a package base given its ID - * - * @param int $base_id The ID of the package base to query - * - * @return string The name of the package base - */ -function pkgbase_name_from_id($base_id) { - $dbh = DB::connect(); - $q = "SELECT Name FROM PackageBases WHERE ID = " . intval($base_id); - $result = $dbh->query($q); - return $result->fetch(PDO::FETCH_COLUMN, 0); -} - -/** - * Get the names of all packages belonging to a package base - * - * @param int $base_id The ID of the package base - * - * @return array The names of all packages belonging to the package base - */ -function pkgbase_get_pkgnames($base_id) { - $dbh = DB::connect(); - $q = "SELECT Name FROM Packages WHERE PackageBaseID = " . intval($base_id); - $result = $dbh->query($q); - return $result->fetchAll(PDO::FETCH_COLUMN, 0); -} - -/** - * Determine whether a package base is (or contains a) VCS package - * - * @param int $base_id The ID of the package base - * - * @return bool True if the package base is/contains a VCS package - */ -function pkgbase_is_vcs($base_id) { - $suffixes = array("-cvs", "-svn", "-git", "-hg", "-bzr", "-darcs"); - $haystack = pkgbase_get_pkgnames($base_id); - array_push($haystack, pkgbase_name_from_id($base_id)); - foreach ($haystack as $pkgname) { - foreach ($suffixes as $suffix) { - if (substr_compare($pkgname, $suffix, -strlen($suffix)) === 0) { - return true; - } - } - } - return false; -} - -/** - * Delete all packages belonging to a package base - * - * @param int $base_id The ID of the package base - * - * @return void - */ -function pkgbase_delete_packages($base_id) { - $dbh = DB::connect(); - $q = "DELETE FROM Packages WHERE PackageBaseID = " . intval($base_id); - $dbh->exec($q); -} - -/** - * Retrieve the maintainer of a package base given its ID - * - * @param int $base_id The ID of the package base to query - * - * @return int The user ID of the current package maintainer - */ -function pkgbase_maintainer_uid($base_id) { - $dbh = DB::connect(); - $q = "SELECT MaintainerUID FROM PackageBases WHERE ID = " . intval($base_id); - $result = $dbh->query($q); - return $result->fetch(PDO::FETCH_COLUMN, 0); -} - -/** - * Retrieve the maintainers of an array of package bases given by their ID - * - * @param int $base_ids The array of IDs of the package bases to query - * - * @return int The user ID of the current package maintainer - */ -function pkgbase_maintainer_uids($base_ids) { - $dbh = DB::connect(); - $q = "SELECT MaintainerUID FROM PackageBases WHERE ID IN (" . implode(",", $base_ids) . ")"; - $result = $dbh->query($q); - return $result->fetchAll(PDO::FETCH_COLUMN, 0); -} - -/** - * Flag package(s) as out-of-date - * - * @param array $base_ids Array of package base IDs to flag/unflag - * @param string $comment The comment to add - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_flag($base_ids, $comment) { - if (!has_credential(CRED_PKGBASE_FLAG)) { - return array(false, __("You must be logged in before you can flag packages.")); - } - - $base_ids = sanitize_ids($base_ids); - if (empty($base_ids)) { - return array(false, __("You did not select any packages to flag.")); - } - - if (strlen($comment) < 3) { - return array(false, __("The selected packages have not been flagged, please enter a comment.")); - } - - $uid = uid_from_sid($_COOKIE['AURSID']); - $dbh = DB::connect(); - - $q = "UPDATE PackageBases SET "; - $q.= "OutOfDateTS = " . strval(time()) . ", FlaggerUID = " . $uid . ", "; - $q.= "FlaggerComment = " . $dbh->quote($comment) . " "; - $q.= "WHERE ID IN (" . implode(",", $base_ids) . ") "; - $q.= "AND OutOfDateTS IS NULL"; - $dbh->exec($q); - - foreach ($base_ids as $base_id) { - notify(array('flag', $uid, $base_id)); - } - - return array(true, __("The selected packages have been flagged out-of-date.")); -} - -/** - * Unflag package(s) as out-of-date - * - * @param array $base_ids Array of package base IDs to flag/unflag - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_unflag($base_ids) { - $uid = uid_from_sid($_COOKIE["AURSID"]); - if (!$uid) { - return array(false, __("You must be logged in before you can unflag packages.")); - } - - $base_ids = sanitize_ids($base_ids); - if (empty($base_ids)) { - return array(false, __("You did not select any packages to unflag.")); - } - - $dbh = DB::connect(); - - $q = "UPDATE PackageBases SET "; - $q.= "OutOfDateTS = NULL "; - $q.= "WHERE ID IN (" . implode(",", $base_ids) . ") "; - - $maintainers = array_merge(pkgbase_maintainer_uids($base_ids), pkgbase_get_comaintainer_uids($base_ids)); - if (!has_credential(CRED_PKGBASE_UNFLAG, $maintainers)) { - $q.= "AND (MaintainerUID = " . $uid . " OR FlaggerUID = " . $uid. ")"; - } - - $result = $dbh->exec($q); - - if ($result) { - return array(true, __("The selected packages have been unflagged.")); - } -} - -/** - * Get package flag OOD comment - * - * @param int $base_id - * - * @return array Tuple of pkgbase ID, reason for OOD, and user who flagged - */ -function pkgbase_get_flag_comment($base_id) { - $base_id = intval($base_id); - $dbh = DB::connect(); - - $q = "SELECT FlaggerComment,OutOfDateTS,Username FROM PackageBases "; - $q.= "LEFT JOIN Users ON FlaggerUID = Users.ID "; - $q.= "WHERE PackageBases.ID = " . $base_id . " "; - $q.= "AND PackageBases.OutOfDateTS IS NOT NULL"; - $result = $dbh->query($q); - - $row = array(); - - if (!$result) { - $row['error'] = __("Error retrieving package details."); - } - else { - $row = $result->fetch(PDO::FETCH_ASSOC); - if (empty($row)) { - $row['error'] = __("Package details could not be found."); - } - } - - return $row; -} - -/** - * Delete package bases - * - * @param array $base_ids Array of package base IDs to delete - * @param int $merge_base_id Package base to merge the deleted ones into - * @param int $via Package request to close upon deletion - * @param bool $grant Allow anyone to delete the package base - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_delete ($base_ids, $merge_base_id, $via, $grant=false) { - if (!$grant && !has_credential(CRED_PKGBASE_DELETE)) { - return array(false, __("You do not have permission to delete packages.")); - } - - $base_ids = sanitize_ids($base_ids); - if (empty($base_ids)) { - return array(false, __("You did not select any packages to delete.")); - } - - $dbh = DB::connect(); - - if ($merge_base_id) { - $merge_base_name = pkgbase_name_from_id($merge_base_id); - } - - $uid = uid_from_sid($_COOKIE['AURSID']); - foreach ($base_ids as $base_id) { - if ($merge_base_id) { - notify(array('delete', $uid, $base_id, $merge_base_id)); - } else { - notify(array('delete', $uid, $base_id)); - } - } - - /* - * Close package request if the deletion was initiated through the - * request interface. NOTE: This needs to happen *before* the actual - * deletion. Otherwise, the former maintainer will not be included in - * the Cc list of the request notification email. - */ - if ($via) { - pkgreq_close(intval($via), 'accepted', ''); - } - - /* Scan through pending deletion requests and close them. */ - $username = username_from_sid($_COOKIE['AURSID']); - foreach ($base_ids as $base_id) { - $pkgreq_ids = array_merge(pkgreq_by_pkgbase($base_id)); - foreach ($pkgreq_ids as $pkgreq_id) { - pkgreq_close(intval($pkgreq_id), 'accepted', - 'The user ' . $username . - ' deleted the package.', true); - } - } - - if ($merge_base_id) { - /* Merge comments */ - $q = "UPDATE PackageComments "; - $q.= "SET PackageBaseID = " . intval($merge_base_id) . " "; - $q.= "WHERE PackageBaseID IN (" . implode(",", $base_ids) . ")"; - $dbh->exec($q); - - /* Merge notifications */ - $q = "SELECT DISTINCT UserID FROM PackageNotifications cn "; - $q.= "WHERE PackageBaseID IN (" . implode(",", $base_ids) . ") "; - $q.= "AND NOT EXISTS (SELECT * FROM PackageNotifications cn2 "; - $q.= "WHERE cn2.PackageBaseID = " . intval($merge_base_id) . " "; - $q.= "AND cn2.UserID = cn.UserID)"; - $result = $dbh->query($q); - - while ($notify_uid = $result->fetch(PDO::FETCH_COLUMN, 0)) { - $q = "INSERT INTO PackageNotifications (UserID, PackageBaseID) "; - $q.= "VALUES (" . intval($notify_uid) . ", " . intval($merge_base_id) . ")"; - $dbh->exec($q); - } - - /* Merge votes */ - foreach ($base_ids as $base_id) { - $q = "UPDATE PackageVotes "; - $q.= "SET PackageBaseID = " . intval($merge_base_id) . " "; - $q.= "WHERE PackageBaseID = " . $base_id . " "; - $q.= "AND UsersID NOT IN ("; - $q.= "SELECT * FROM (SELECT UsersID "; - $q.= "FROM PackageVotes "; - $q.= "WHERE PackageBaseID = " . intval($merge_base_id); - $q.= ") temp)"; - $dbh->exec($q); - } - - $q = "UPDATE PackageBases "; - $q.= "SET NumVotes = (SELECT COUNT(*) FROM PackageVotes "; - $q.= "WHERE PackageBaseID = " . intval($merge_base_id) . ") "; - $q.= "WHERE ID = " . intval($merge_base_id); - $dbh->exec($q); - } - - $q = "DELETE FROM Packages WHERE PackageBaseID IN (" . implode(",", $base_ids) . ")"; - $dbh->exec($q); - - $q = "DELETE FROM PackageBases WHERE ID IN (" . implode(",", $base_ids) . ")"; - $dbh->exec($q); - - return array(true, __("The selected packages have been deleted.")); -} - -/** - * Adopt or disown packages - * - * @param array $base_ids Array of package base IDs to adopt/disown - * @param bool $action Adopts if true, disowns if false. Adopts by default - * @param int $via Package request to close upon adoption - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_adopt ($base_ids, $action=true, $via) { - $dbh = DB::connect(); - - $uid = uid_from_sid($_COOKIE["AURSID"]); - if (!$uid) { - if ($action) { - return array(false, __("You must be logged in before you can adopt packages.")); - } else { - return array(false, __("You must be logged in before you can disown packages.")); - } - } - - /* Verify package ownership. */ - $base_ids = sanitize_ids($base_ids); - - $q = "SELECT ID FROM PackageBases "; - $q.= "WHERE ID IN (" . implode(",", $base_ids) . ") "; - - if ($action && !has_credential(CRED_PKGBASE_ADOPT)) { - /* Regular users may only adopt orphan packages. */ - $q.= "AND MaintainerUID IS NULL"; - } - if (!$action && !has_credential(CRED_PKGBASE_DISOWN)) { - /* Regular users may only disown their own packages. */ - $q.= "AND MaintainerUID = " . $uid; - } - - $result = $dbh->query($q); - $base_ids = $result->fetchAll(PDO::FETCH_COLUMN, 0); - - /* Error out if the list of remaining packages is empty. */ - if (empty($base_ids)) { - if ($action) { - return array(false, __("You did not select any packages to adopt.")); - } else { - return array(false, __("You did not select any packages to disown.")); - } - } - - /* - * Close package request if the disownment was initiated through the - * request interface. NOTE: This needs to happen *before* the actual - * disown operation. Otherwise, the former maintainer will not be - * included in the Cc list of the request notification email. - */ - if ($via) { - pkgreq_close(intval($via), 'accepted', ''); - } - - /* Scan through pending orphan requests and close them. */ - if (!$action) { - $username = username_from_sid($_COOKIE['AURSID']); - foreach ($base_ids as $base_id) { - $pkgreq_ids = pkgreq_by_pkgbase($base_id, 'orphan'); - foreach ($pkgreq_ids as $pkgreq_id) { - pkgreq_close(intval($pkgreq_id), 'accepted', - 'The user ' . $username . - ' disowned the package.', true); - } - } - } - - /* Adopt or disown the package. */ - if ($action) { - $q = "UPDATE PackageBases "; - $q.= "SET MaintainerUID = $uid "; - $q.= "WHERE ID IN (" . implode(",", $base_ids) . ") "; - $dbh->exec($q); - - /* Add the new maintainer to the notification list. */ - pkgbase_notify($base_ids); - } else { - /* Update the co-maintainer list when disowning a package. */ - if (has_credential(CRED_PKGBASE_DISOWN)) { - foreach ($base_ids as $base_id) { - pkgbase_set_comaintainers($base_id, array()); - } - - $q = "UPDATE PackageBases "; - $q.= "SET MaintainerUID = NULL "; - $q.= "WHERE ID IN (" . implode(",", $base_ids) . ") "; - $dbh->exec($q); - } else { - foreach ($base_ids as $base_id) { - $comaintainers = pkgbase_get_comaintainers($base_id); - - if (count($comaintainers) > 0) { - $comaintainer_uid = uid_from_username($comaintainers[0]); - $comaintainers = array_diff($comaintainers, array($comaintainers[0])); - pkgbase_set_comaintainers($base_id, $comaintainers); - } else { - $comaintainer_uid = "NULL"; - } - - $q = "UPDATE PackageBases "; - $q.= "SET MaintainerUID = " . $comaintainer_uid . " "; - $q.= "WHERE ID = " . $base_id; - $dbh->exec($q); - } - } - } - - foreach ($base_ids as $base_id) { - notify(array($action ? 'adopt' : 'disown', $uid, $base_id)); - } - - if ($action) { - return array(true, __("The selected packages have been adopted.")); - } else { - return array(true, __("The selected packages have been disowned.")); - } -} - -/** - * Vote and un-vote for packages - * - * @param array $base_ids Array of package base IDs to vote/un-vote - * @param bool $action Votes if true, un-votes if false. Votes by default - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_vote ($base_ids, $action=true) { - if (!has_credential(CRED_PKGBASE_VOTE)) { - if ($action) { - return array(false, __("You must be logged in before you can vote for packages.")); - } else { - return array(false, __("You must be logged in before you can un-vote for packages.")); - } - } - - $base_ids = sanitize_ids($base_ids); - if (empty($base_ids)) { - if ($action) { - return array(false, __("You did not select any packages to vote for.")); - } else { - return array(false, __("Your votes have been removed from the selected packages.")); - } - } - - $dbh = DB::connect(); - $my_votes = pkgbase_votes_from_sid($_COOKIE["AURSID"]); - $uid = uid_from_sid($_COOKIE["AURSID"]); - - $first = 1; - $vote_ids = ""; - $vote_clauses = ""; - foreach ($base_ids as $pid) { - if ($action) { - $check = !isset($my_votes[$pid]); - } else { - $check = isset($my_votes[$pid]); - } - - if ($check) { - if ($first) { - $first = 0; - $vote_ids = $pid; - if ($action) { - $vote_clauses = "($uid, $pid, " . strval(time()) . ")"; - } - } else { - $vote_ids .= ", $pid"; - if ($action) { - $vote_clauses .= ", ($uid, $pid, " . strval(time()) . ")"; - } - } - } - } - - if (!empty($vote_ids)) { - /* Only add votes for packages the user hasn't already voted for. */ - $op = $action ? "+" : "-"; - $q = "UPDATE PackageBases SET NumVotes = NumVotes $op 1 "; - $q.= "WHERE ID IN ($vote_ids)"; - - $dbh->exec($q); - - if ($action) { - $q = "INSERT INTO PackageVotes (UsersID, PackageBaseID, VoteTS) VALUES "; - $q.= $vote_clauses; - } else { - $q = "DELETE FROM PackageVotes WHERE UsersID = $uid "; - $q.= "AND PackageBaseID IN ($vote_ids)"; - } - - $dbh->exec($q); - } - - if ($action) { - return array(true, __("Your votes have been cast for the selected packages.")); - } else { - return array(true, __("Your votes have been removed from the selected packages.")); - } -} - -/** - * Get all usernames and IDs that voted for a specific package base - * - * @param string $pkgbase_name The package base to retrieve votes for - * - * @return array User IDs and usernames that voted for a specific package base - */ -function pkgbase_votes_from_name($pkgbase_name) { - $dbh = DB::connect(); - - $q = "SELECT UsersID, Username, Name, VoteTS FROM PackageVotes "; - $q.= "LEFT JOIN Users ON UsersID = Users.ID "; - $q.= "LEFT JOIN PackageBases "; - $q.= "ON PackageVotes.PackageBaseID = PackageBases.ID "; - $q.= "WHERE PackageBases.Name = ". $dbh->quote($pkgbase_name) . " "; - $q.= "ORDER BY Username"; - $result = $dbh->query($q); - - if (!$result) { - return; - } - - $votes = array(); - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $votes[] = $row; - } - - return $votes; -} - -/** - * Determine if a user has already voted for a specific package base - * - * @param string $uid The user ID to check for an existing vote - * @param string $base_id The package base ID to check for an existing vote - * - * @return bool True if the user has already voted, otherwise false - */ -function pkgbase_user_voted($uid, $base_id) { - $dbh = DB::connect(); - $q = "SELECT COUNT(*) FROM PackageVotes WHERE "; - $q.= "UsersID = ". $dbh->quote($uid) . " AND "; - $q.= "PackageBaseID = " . $dbh->quote($base_id); - $result = $dbh->query($q); - if (!$result) { - return null; - } - - return ($result->fetch(PDO::FETCH_COLUMN, 0) > 0); -} - -/** - * Determine if a user wants notifications for a specific package base - * - * @param string $uid User ID to check in the database - * @param string $base_id Package base ID to check notifications for - * - * @return bool True if the user wants notifications, otherwise false - */ -function pkgbase_user_notify($uid, $base_id) { - $dbh = DB::connect(); - - $q = "SELECT * FROM PackageNotifications WHERE UserID = " . $dbh->quote($uid); - $q.= " AND PackageBaseID = " . $dbh->quote($base_id); - $result = $dbh->query($q); - - if (!$result) { - return false; - } - - return ($result->fetch(PDO::FETCH_NUM) > 0); -} - -/** - * Toggle notification of packages - * - * @param array $base_ids Array of package base IDs to toggle - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_notify ($base_ids, $action=true) { - if (!has_credential(CRED_PKGBASE_NOTIFY)) { - return; - } - - $base_ids = sanitize_ids($base_ids); - if (empty($base_ids)) { - return array(false, __("Couldn't add to notification list.")); - } - - $dbh = DB::connect(); - $uid = uid_from_sid($_COOKIE["AURSID"]); - - $output = ""; - - $first = true; - - /* - * There currently shouldn't be multiple requests here, but the format - * in which it's sent requires this. - */ - foreach ($base_ids as $bid) { - $q = "SELECT Name FROM PackageBases WHERE ID = $bid"; - $result = $dbh->query($q); - if ($result) { - $row = $result->fetch(PDO::FETCH_NUM); - $basename = $row[0]; - } - else { - $basename = ''; - } - - if ($first) - $first = false; - else - $output .= ", "; - - - if ($action) { - $q = "SELECT COUNT(*) FROM PackageNotifications WHERE "; - $q .= "UserID = $uid AND PackageBaseID = $bid"; - - /* Notification already added. Don't add again. */ - $result = $dbh->query($q); - if ($result->fetchColumn() == 0) { - $q = "INSERT INTO PackageNotifications (PackageBaseID, UserID) VALUES ($bid, $uid)"; - $dbh->exec($q); - } - - $output .= $basename; - } - else { - $q = "DELETE FROM PackageNotifications WHERE PackageBaseID = $bid "; - $q .= "AND UserID = $uid"; - $dbh->exec($q); - - $output .= $basename; - } - } - - if ($action) { - $output = __("You have been added to the comment notification list for %s.", $output); - } - else { - $output = __("You have been removed from the comment notification list for %s.", $output); - } - - return array(true, $output); -} - -/** - * Delete a package comment - * - * @param boolean $undelete True if undeleting rather than deleting - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_delete_comment($undelete=false) { - $uid = uid_from_sid($_COOKIE["AURSID"]); - if (!$uid) { - return array(false, __("You must be logged in before you can edit package information.")); - } - - if (isset($_POST["comment_id"])) { - $comment_id = $_POST["comment_id"]; - } else { - return array(false, __("Missing comment ID.")); - } - - $dbh = DB::connect(); - if ($undelete) { - if (!has_credential(CRED_COMMENT_UNDELETE)) { - return array(false, __("You are not allowed to undelete this comment.")); - } - - $q = "UPDATE PackageComments "; - $q.= "SET DelUsersID = NULL, "; - $q.= "DelTS = NULL "; - $q.= "WHERE ID = ".intval($comment_id); - $dbh->exec($q); - return array(true, __("Comment has been undeleted.")); - } else { - if (!can_delete_comment($comment_id)) { - return array(false, __("You are not allowed to delete this comment.")); - } - - $q = "UPDATE PackageComments "; - $q.= "SET DelUsersID = ".$uid.", "; - $q.= "DelTS = " . strval(time()) . " "; - $q.= "WHERE ID = ".intval($comment_id); - $dbh->exec($q); - return array(true, __("Comment has been deleted.")); - } -} - -/** - * Edit a package comment - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_edit_comment($comment) { - $uid = uid_from_sid($_COOKIE["AURSID"]); - if (!$uid) { - return array(false, __("You must be logged in before you can edit package information.")); - } - - if (isset($_POST["comment_id"])) { - $comment_id = $_POST["comment_id"]; - } else { - return array(false, __("Missing comment ID.")); - } - - if (trim($comment) == '') { - return array(false, __('Comment cannot be empty.')); - } - - $dbh = DB::connect(); - if (can_edit_comment($comment_id)) { - $q = "UPDATE PackageComments "; - $q.= "SET EditedUsersID = ".$uid.", "; - $q.= "Comments = ".$dbh->quote($comment).", "; - $q.= "EditedTS = " . strval(time()) . " "; - $q.= "WHERE ID = ".intval($comment_id); - $dbh->exec($q); - - render_comment($comment_id); - - return array(true, __("Comment has been edited.")); - } else { - return array(false, __("You are not allowed to edit this comment.")); - } -} - -/** - * Get a list of package base keywords - * - * @param int $base_id The package base ID to retrieve the keywords for - * - * @return array An array of keywords - */ -function pkgbase_get_keywords($base_id) { - $dbh = DB::connect(); - $q = "SELECT Keyword FROM PackageKeywords "; - $q .= "WHERE PackageBaseID = " . intval($base_id) . " "; - $q .= "ORDER BY Keyword ASC"; - $result = $dbh->query($q); - - if ($result) { - return $result->fetchAll(PDO::FETCH_COLUMN, 0); - } else { - return array(); - } -} - -/** - * Update the list of keywords of a package base - * - * @param int $base_id The package base ID to update the keywords of - * @param array $users Array of keywords - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_set_keywords($base_id, $keywords) { - $base_id = intval($base_id); - - $maintainers = array_merge(array(pkgbase_maintainer_uid($base_id)), pkgbase_get_comaintainer_uids(array($base_id))); - if (!has_credential(CRED_PKGBASE_SET_KEYWORDS, $maintainers)) { - return array(false, __("You are not allowed to edit the keywords of this package base.")); - } - - /* Remove empty and duplicate user names. */ - $keywords = array_unique(array_filter(array_map('trim', $keywords))); - - $dbh = DB::connect(); - - $q = sprintf("DELETE FROM PackageKeywords WHERE PackageBaseID = %d", $base_id); - $dbh->exec($q); - - $i = 0; - foreach ($keywords as $keyword) { - $q = sprintf("INSERT INTO PackageKeywords (PackageBaseID, Keyword) VALUES (%d, %s)", $base_id, $dbh->quote($keyword)); - $dbh->exec($q); - - $i++; - if ($i >= 20) { - break; - } - } - - return array(true, __("The package base keywords have been updated.")); -} - -/** - * Get a list of package base co-maintainers - * - * @param int $base_id The package base ID to retrieve the co-maintainers for - * - * @return array An array of co-maintainer user names - */ -function pkgbase_get_comaintainers($base_id) { - $dbh = DB::connect(); - $q = "SELECT UserName FROM PackageComaintainers "; - $q .= "INNER JOIN Users ON Users.ID = PackageComaintainers.UsersID "; - $q .= "WHERE PackageComaintainers.PackageBaseID = " . intval($base_id) . " "; - $q .= "ORDER BY Priority ASC"; - $result = $dbh->query($q); - - if ($result) { - return $result->fetchAll(PDO::FETCH_COLUMN, 0); - } else { - return array(); - } -} - -/** - * Get a list of package base co-maintainer IDs - * - * @param int $base_id The package base ID to retrieve the co-maintainers for - * - * @return array An array of co-maintainer user UDs - */ -function pkgbase_get_comaintainer_uids($base_ids) { - $dbh = DB::connect(); - $q = "SELECT UsersID FROM PackageComaintainers "; - $q .= "INNER JOIN Users ON Users.ID = PackageComaintainers.UsersID "; - $q .= "WHERE PackageComaintainers.PackageBaseID IN (" . implode(",", $base_ids) . ") "; - $q .= "ORDER BY Priority ASC"; - $result = $dbh->query($q); - - if ($result) { - return $result->fetchAll(PDO::FETCH_COLUMN, 0); - } else { - return array(); - } -} - -/** - * Update the list of co-maintainers of a package base - * - * @param int $base_id The package base ID to update the co-maintainers of - * @param array $users Array of co-maintainer user names - * @param boolean $override Override credential check if true - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgbase_set_comaintainers($base_id, $users, $override=false) { - $maintainer_uid = pkgbase_maintainer_uid($base_id); - if (!$override && !has_credential(CRED_PKGBASE_EDIT_COMAINTAINERS, array($maintainer_uid))) { - return array(false, __("You are not allowed to manage co-maintainers of this package base.")); - } - - /* Remove empty and duplicate user names. */ - $users = array_unique(array_filter(array_map('trim', $users))); - - $dbh = DB::connect(); - - $uids_new = array(); - foreach($users as $user) { - $q = "SELECT ID FROM Users "; - $q .= "WHERE UserName = " . $dbh->quote($user); - $result = $dbh->query($q); - $uid = $result->fetchColumn(0); - - if (!$uid) { - return array(false, __("Invalid user name: %s", $user)); - } elseif ($uid == $maintainer_uid) { - // silently ignore when maintainer == co-maintainer - continue; - } else { - $uids_new[] = $uid; - } - } - - $q = sprintf("SELECT UsersID FROM PackageComaintainers WHERE PackageBaseID = %d", $base_id); - $result = $dbh->query($q); - $uids_old = $result->fetchAll(PDO::FETCH_COLUMN, 0); - - $uids_add = array_diff($uids_new, $uids_old); - $uids_rem = array_diff($uids_old, $uids_new); - - $i = 1; - foreach ($uids_new as $uid) { - if (in_array($uid, $uids_add)) { - $q = sprintf("INSERT INTO PackageComaintainers (PackageBaseID, UsersID, Priority) VALUES (%d, %d, %d)", $base_id, $uid, $i); - notify(array('comaintainer-add', $uid, $base_id)); - } else { - $q = sprintf("UPDATE PackageComaintainers SET Priority = %d WHERE PackageBaseID = %d AND UsersID = %d", $i, $base_id, $uid); - } - - $dbh->exec($q); - $i++; - } - - foreach ($uids_rem as $uid) { - $q = sprintf("DELETE FROM PackageComaintainers WHERE PackageBaseID = %d AND UsersID = %d", $base_id, $uid); - $dbh->exec($q); - notify(array('comaintainer-remove', $uid, $base_id)); - } - - return array(true, __("The package base co-maintainers have been updated.")); -} - -function pkgbase_remove_comaintainer($base_id, $uid) { - $uname = username_from_id($uid); - $names = pkgbase_get_comaintainers($base_id); - $names = array_diff($names, array($uname)); - return pkgbase_set_comaintainers($base_id, $names, true); -} diff --git a/web/lib/pkgfuncs.inc.php b/web/lib/pkgfuncs.inc.php deleted file mode 100644 index 140c7ec1..00000000 --- a/web/lib/pkgfuncs.inc.php +++ /dev/null @@ -1,957 +0,0 @@ -query($q); - - if (!$result) { - return false; - } - - $uid = $result->fetch(PDO::FETCH_COLUMN, 0); - - return has_credential(CRED_COMMENT_DELETE, array($uid)); -} - -/** - * Determine if the user can delete a specific package comment using an array - * - * Only the comment submitter, Trusted Users, and Developers can delete - * comments. This function is used for the frontend side of comment deletion. - * - * @param array $comment All database information relating a specific comment - * - * @return bool True if the user can delete the comment, otherwise false - */ -function can_delete_comment_array($comment) { - return has_credential(CRED_COMMENT_DELETE, array($comment['UsersID'])); -} - -/** - * Determine if the user can edit a specific package comment - * - * Only the comment submitter, Trusted Users, and Developers can edit - * comments. This function is used for the backend side of comment editing. - * - * @param string $comment_id The comment ID in the database - * - * @return bool True if the user can edit the comment, otherwise false - */ -function can_edit_comment($comment_id=0) { - $dbh = DB::connect(); - - $q = "SELECT UsersID FROM PackageComments "; - $q.= "WHERE ID = " . intval($comment_id); - $result = $dbh->query($q); - - if (!$result) { - return false; - } - - $uid = $result->fetch(PDO::FETCH_COLUMN, 0); - - return has_credential(CRED_COMMENT_EDIT, array($uid)); -} - -/** - * Determine if the user can edit a specific package comment using an array - * - * Only the comment submitter, Trusted Users, and Developers can edit - * comments. This function is used for the frontend side of comment editing. - * - * @param array $comment All database information relating a specific comment - * - * @return bool True if the user can edit the comment, otherwise false - */ -function can_edit_comment_array($comment) { - return has_credential(CRED_COMMENT_EDIT, array($comment['UsersID'])); -} - -/** - * Determine if the user can pin a specific package comment - * - * Only the Package Maintainer, Package Co-maintainers, Trusted Users, and - * Developers can pin comments. This function is used for the backend side of - * comment pinning. - * - * @param string $comment_id The comment ID in the database - * - * @return bool True if the user can pin the comment, otherwise false - */ -function can_pin_comment($comment_id=0) { - $dbh = DB::connect(); - - $q = "SELECT MaintainerUID FROM PackageBases AS pb "; - $q.= "LEFT JOIN PackageComments AS pc ON pb.ID = pc.PackageBaseID "; - $q.= "WHERE pc.ID = " . intval($comment_id) . " "; - $q.= "UNION "; - $q.= "SELECT pcm.UsersID FROM PackageComaintainers AS pcm "; - $q.= "LEFT JOIN PackageComments AS pc "; - $q.= "ON pcm.PackageBaseID = pc.PackageBaseID "; - $q.= "WHERE pc.ID = " . intval($comment_id); - $result = $dbh->query($q); - - if (!$result) { - return false; - } - - $uids = $result->fetchAll(PDO::FETCH_COLUMN, 0); - - return has_credential(CRED_COMMENT_PIN, $uids); -} - -/** - * Determine if the user can edit a specific package comment using an array - * - * Only the Package Maintainer, Package Co-maintainers, Trusted Users, and - * Developers can pin comments. This function is used for the frontend side of - * comment pinning. - * - * @param array $comment All database information relating a specific comment - * - * @return bool True if the user can edit the comment, otherwise false - */ -function can_pin_comment_array($comment) { - return can_pin_comment($comment['ID']); -} - -/** - * Check to see if the package name already exists in the database - * - * @param string $name The package name to check - * - * @return string|void Package name if it already exists - */ -function pkg_from_name($name="") { - if (!$name) {return NULL;} - $dbh = DB::connect(); - $q = "SELECT ID FROM Packages "; - $q.= "WHERE Name = " . $dbh->quote($name); - $result = $dbh->query($q); - if (!$result) { - return; - } - $row = $result->fetch(PDO::FETCH_NUM); - if ($row) { - return $row[0]; - } -} - -/** - * Get licenses for a specific package - * - * @param int $pkgid The package to get licenses for - * - * @return array All licenses for the package - */ -function pkg_licenses($pkgid) { - $pkgid = intval($pkgid); - if (!$pkgid) { - return array(); - } - $q = "SELECT l.Name FROM Licenses l "; - $q.= "INNER JOIN PackageLicenses pl ON pl.LicenseID = l.ID "; - $q.= "WHERE pl.PackageID = ". $pkgid; - $ttl = config_get_int('options', 'cache_pkginfo_ttl'); - $rows = db_cache_result($q, 'licenses:' . $pkgid, PDO::FETCH_NUM, $ttl); - return array_map(function ($x) { return $x[0]; }, $rows); -} - -/** - * Get package groups for a specific package - * - * @param int $pkgid The package to get groups for - * - * @return array All package groups for the package - */ -function pkg_groups($pkgid) { - $pkgid = intval($pkgid); - if (!$pkgid) { - return array(); - } - $q = "SELECT g.Name FROM `Groups` g "; - $q.= "INNER JOIN PackageGroups pg ON pg.GroupID = g.ID "; - $q.= "WHERE pg.PackageID = ". $pkgid; - $ttl = config_get_int('options', 'cache_pkginfo_ttl'); - $rows = db_cache_result($q, 'groups:' . $pkgid, PDO::FETCH_NUM, $ttl); - return array_map(function ($x) { return $x[0]; }, $rows); -} - -/** - * Get providers for a specific package - * - * @param string $name The name of the "package" to get providers for - * - * @return array The IDs and names of all providers of the package - */ -function pkg_providers($name) { - $dbh = DB::connect(); - $q = "SELECT p.ID, p.Name FROM Packages p "; - $q.= "WHERE p.Name = " . $dbh->quote($name) . " "; - $q.= "UNION "; - $q.= "SELECT p.ID, p.Name FROM Packages p "; - $q.= "LEFT JOIN PackageRelations pr ON pr.PackageID = p.ID "; - $q.= "LEFT JOIN RelationTypes rt ON rt.ID = pr.RelTypeID "; - $q.= "WHERE (rt.Name = 'provides' "; - $q.= "AND pr.RelName = " . $dbh->quote($name) . ")"; - $q.= "UNION "; - $q.= "SELECT 0, Name FROM OfficialProviders "; - $q.= "WHERE Provides = " . $dbh->quote($name); - $ttl = config_get_int('options', 'cache_pkginfo_ttl'); - return db_cache_result($q, 'providers:' . $name, PDO::FETCH_NUM, $ttl); -} - -/** - * Get package dependencies for a specific package - * - * @param int $pkgid The package to get dependencies for - * @param int $limit An upper bound for the number of packages to retrieve - * - * @return array All package dependencies for the package - */ -function pkg_dependencies($pkgid, $limit) { - $pkgid = intval($pkgid); - if (!$pkgid) { - return array(); - } - $q = "SELECT pd.DepName, dt.Name, pd.DepDesc, "; - $q.= "pd.DepCondition, pd.DepArch, p.ID "; - $q.= "FROM PackageDepends pd "; - $q.= "LEFT JOIN Packages p ON pd.DepName = p.Name "; - $q.= "LEFT JOIN DependencyTypes dt ON dt.ID = pd.DepTypeID "; - $q.= "WHERE pd.PackageID = ". $pkgid . " "; - $q.= "ORDER BY pd.DepName LIMIT " . intval($limit); - $ttl = config_get_int('options', 'cache_pkginfo_ttl'); - return db_cache_result($q, 'dependencies:' . $pkgid, PDO::FETCH_NUM, $ttl); -} - -/** - * Get package relations for a specific package - * - * @param int $pkgid The package to get relations for - * - * @return array All package relations for the package - */ -function pkg_relations($pkgid) { - $pkgid = intval($pkgid); - if (!$pkgid) { - return array(); - } - $q = "SELECT pr.RelName, rt.Name, pr.RelCondition, pr.RelArch, p.ID FROM PackageRelations pr "; - $q.= "LEFT JOIN Packages p ON pr.RelName = p.Name "; - $q.= "LEFT JOIN RelationTypes rt ON rt.ID = pr.RelTypeID "; - $q.= "WHERE pr.PackageID = ". $pkgid . " "; - $q.= "ORDER BY pr.RelName"; - $ttl = config_get_int('options', 'cache_pkginfo_ttl'); - return db_cache_result($q, 'relations:' . $pkgid, PDO::FETCH_NUM, $ttl); -} - -/** - * Get the HTML code to display a package dependency link annotation - * (dependency type, architecture, ...) - * - * @param string $type The name of the dependency type - * @param string $arch The package dependency architecture - * @param string $desc An optdepends description - * - * @return string The HTML code of the label to display - */ -function pkg_deplink_annotation($type, $arch, $desc=false) { - if ($type == 'depends' && !$arch) { - return ''; - } - - $link = ' ('; - - if ($type == 'makedepends') { - $link .= 'make'; - } elseif ($type == 'checkdepends') { - $link .= 'check'; - } elseif ($type == 'optdepends') { - $link .= 'optional'; - } - - if ($type != 'depends' && $arch) { - $link .= ', '; - } - - if ($arch) { - $link .= htmlspecialchars($arch); - } - - $link .= ')'; - if ($type == 'optdepends' && $desc) { - $link .= ' – ' . htmlspecialchars($desc) . ' '; - } - $link .= ''; - - return $link; -} - -/** - * Get the HTML code to display a package provider link - * - * @param string $name The name of the provider - * @param bool $official True if the package is in the official repositories - * - * @return string The HTML code of the link to display - */ -function pkg_provider_link($name, $official) { - $link = ''; - $link .= htmlspecialchars($name) . ''; - - return $link; -} - -/** - * Get the HTML code to display a package dependency link - * - * @param string $name The name of the dependency - * @param string $type The name of the dependency type - * @param string $desc The (optional) description of the dependency - * @param string $cond The package dependency condition string - * @param string $arch The package dependency architecture - * @param int $pkg_id The package of the package to display the dependency for - * - * @return string The HTML code of the label to display - */ -function pkg_depend_link($name, $type, $desc, $cond, $arch, $pkg_id) { - /* - * TODO: We currently perform one SQL query per nonexistent package - * dependency. It would be much better if we could annotate dependency - * data with providers so that we already know whether a dependency is - * a "provision name" or a package from the official repositories at - * this point. - */ - $providers = pkg_providers($name); - - if (count($providers) == 0) { - $link = ''; - $link .= htmlspecialchars($name); - $link .= ''; - $link .= htmlspecialchars($cond) . ' '; - $link .= pkg_deplink_annotation($type, $arch, $desc); - return $link; - } - - $link = htmlspecialchars($name); - foreach ($providers as $provider) { - if ($provider[1] == $name) { - $is_official = ($provider[0] == 0); - $name = $provider[1]; - $link = pkg_provider_link($name, $is_official); - break; - } - } - $link .= htmlspecialchars($cond) . ' '; - - foreach ($providers as $key => $provider) { - if ($provider[1] == $name) { - unset($providers[$key]); - } - } - - if (count($providers) > 0) { - $link .= '('; - foreach ($providers as $provider) { - $is_official = ($provider[0] == 0); - $name = $provider[1]; - $link .= pkg_provider_link($name, $is_official) . ', '; - } - $link = substr($link, 0, -2); - $link .= ')'; - } - - $link .= pkg_deplink_annotation($type, $arch, $desc); - - return $link; -} - -/** - * Get the HTML code to display a package requirement link - * - * @param string $name The name of the requirement - * @param string $depends The (literal) name of the dependency of $name - * @param string $type The name of the dependency type - * @param string $arch The package dependency architecture - * @param string $pkgname The name of dependant package - * - * @return string The HTML code of the link to display - */ -function pkg_requiredby_link($name, $depends, $type, $arch, $pkgname) { - $link = ''; - $link .= htmlspecialchars($name) . ''; - - if ($depends != $pkgname) { - $link .= ' ('; - $link .= __('requires %s', htmlspecialchars($depends)); - $link .= ')'; - } - - return $link . pkg_deplink_annotation($type, $arch); -} - -/** - * Get the HTML code to display a package relation - * - * @param string $name The name of the relation - * @param string $cond The package relation condition string - * @param string $arch The package relation architecture - * - * @return string The HTML code of the label to display - */ -function pkg_rel_html($name, $cond, $arch) { - $html = htmlspecialchars($name) . htmlspecialchars($cond); - - if ($arch) { - $html .= ' (' . htmlspecialchars($arch) . ')'; - } - - return $html; -} - -/** - * Get the HTML code to display a source link - * - * @param string $url The URL of the source - * @param string $arch The source architecture - * @param string $package The name of the package - * - * @return string The HTML code of the label to display - */ -function pkg_source_link($url, $arch, $package) { - $url = explode('::', $url); - $parsed_url = parse_url($url[0]); - - if (isset($parsed_url['scheme']) || isset($url[1])) { - $link = '' . htmlspecialchars($url[0]) . ''; - } else { - $file_url = sprintf(config_get('options', 'source_file_uri'), htmlspecialchars($url[0]), $package); - $link = '' . htmlspecialchars($url[0]) . ''; - } - - if ($arch) { - $link .= ' (' . htmlspecialchars($arch) . ')'; - } - - return $link; -} - -/** - * Determine packages that depend on a package - * - * @param string $name The package name for the dependency search - * @param array $provides A list of virtual provisions of the package - * @param int $limit An upper bound for the number of packages to retrieve - * - * @return array All packages that depend on the specified package name - */ -function pkg_required($name="", $provides, $limit) { - $deps = array(); - if ($name != "") { - $dbh = DB::connect(); - - $name_list = $dbh->quote($name); - foreach ($provides as $p) { - $name_list .= ',' . $dbh->quote($p[0]); - } - - $q = "SELECT p.Name, pd.DepName, dt.Name, pd.DepArch "; - $q.= "FROM PackageDepends pd "; - $q.= "LEFT JOIN Packages p ON p.ID = pd.PackageID "; - $q.= "LEFT JOIN DependencyTypes dt ON dt.ID = pd.DepTypeID "; - $q.= "WHERE pd.DepName IN (" . $name_list . ") "; - $q.= "ORDER BY p.Name LIMIT " . intval($limit); - /* Not invalidated by package updates. */ - return db_cache_result($q, 'required:' . $name, PDO::FETCH_NUM); - } - return $deps; -} - -/** - * Get all package sources for a specific package - * - * @param string $pkgid The package ID to get the sources for - * - * @return array All sources associated with a specific package - */ -function pkg_sources($pkgid) { - $pkgid = intval($pkgid); - if (!$pkgid) { - return array(); - } - $q = "SELECT Source, SourceArch FROM PackageSources "; - $q.= "WHERE PackageID = " . $pkgid; - $q.= " ORDER BY Source"; - $ttl = config_get_int('options', 'cache_pkginfo_ttl'); - return db_cache_result($q, 'required:' . $pkgid, PDO::FETCH_NUM, $ttl); -} - -/** - * Get the package details - * - * @param string $id The package ID to get description for - * - * @return array The package's details OR error message - **/ -function pkg_get_details($id=0) { - $dbh = DB::connect(); - - $q = "SELECT Packages.*, PackageBases.ID AS BaseID, "; - $q.= "PackageBases.Name AS BaseName, PackageBases.NumVotes, "; - $q.= "PackageBases.Popularity, PackageBases.OutOfDateTS, "; - $q.= "PackageBases.SubmittedTS, PackageBases.ModifiedTS, "; - $q.= "PackageBases.SubmitterUID, PackageBases.MaintainerUID, "; - $q.= "PackageBases.PackagerUID, PackageBases.FlaggerUID, "; - $q.= "(SELECT COUNT(*) FROM PackageRequests "; - $q.= " WHERE PackageRequests.PackageBaseID = Packages.PackageBaseID "; - $q.= " AND PackageRequests.Status = 0) AS RequestCount "; - $q.= "FROM Packages, PackageBases "; - $q.= "WHERE PackageBases.ID = Packages.PackageBaseID "; - $q.= "AND Packages.ID = " . intval($id); - $result = $dbh->query($q); - - $row = array(); - - if (!$result) { - $row['error'] = __("Error retrieving package details."); - } - else { - $row = $result->fetch(PDO::FETCH_ASSOC); - if (empty($row)) { - $row['error'] = __("Package details could not be found."); - } - } - - return $row; -} - -/** - * Display the package details page - * - * @param string $id The package ID to get details page for - * @param array $row Package details retrieved by pkg_get_details() - * @param string $SID The session ID of the visitor - * - * @return void - */ -function pkg_display_details($id=0, $row, $SID="") { - $dbh = DB::connect(); - - if (isset($row['error'])) { - print "

    " . $row['error'] . "

    \n"; - } - else { - $base_id = pkgbase_from_pkgid($id); - $pkgbase_name = pkgbase_name_from_id($base_id); - - include('pkg_details.php'); - - if ($SID) { - include('pkg_comment_box.php'); - } - - $include_deleted = has_credential(CRED_COMMENT_VIEW_DELETED); - - $limit_pinned = isset($_GET['pinned']) ? 0 : 5; - $pinned = pkgbase_comments($base_id, $limit_pinned, false, true); - if (!empty($pinned)) { - $comment_section = "package"; - include('pkg_comments.php'); - } - unset($pinned); - - - $total_comment_count = pkgbase_comments_count($base_id, $include_deleted); - list($pagination_templs, $per_page, $offset) = calculate_pagination($total_comment_count); - - $comments = pkgbase_comments($base_id, $per_page, $include_deleted, false, $offset); - if (!empty($comments)) { - $comment_section = "package"; - include('pkg_comments.php'); - } - } -} - -/** - * Output the body of the search results page - * - * @param array $params Search parameters - * @param bool $show_headers True if statistics should be included - * @param string $SID The session ID of the visitor - * - * @return int The total number of packages matching the query - */ -function pkg_search_page($params, $show_headers=true, $SID="") { - $dbh = DB::connect(); - - /* - * Get commonly used variables. - * TODO: Reduce the number of database queries! - */ - if ($SID) - $myuid = uid_from_sid($SID); - - /* Sanitize paging variables. */ - if (isset($params['O'])) { - $params['O'] = bound(intval($params['O']), 0, 2500); - } else { - $params['O'] = 0; - } - - if (isset($params["PP"])) { - $params["PP"] = bound(intval($params["PP"]), 50, 250); - } else { - $params["PP"] = 50; - } - - /* - * FIXME: Pull out DB-related code. All of it! This one's worth a - * choco-chip cookie, one of those nice big soft ones. - */ - - /* Build the package search query. */ - $q_select = "SELECT "; - if ($SID) { - $q_select .= "PackageNotifications.UserID AS Notify, - PackageVotes.UsersID AS Voted, "; - } - $q_select .= "Users.Username AS Maintainer, - Packages.Name, Packages.Version, Packages.Description, - PackageBases.NumVotes, PackageBases.Popularity, Packages.ID, - Packages.PackageBaseID, PackageBases.OutOfDateTS "; - - $q_from = "FROM Packages - LEFT JOIN PackageBases ON (PackageBases.ID = Packages.PackageBaseID) - LEFT JOIN Users ON (PackageBases.MaintainerUID = Users.ID) "; - if ($SID) { - /* This is not needed for the total row count query. */ - $q_from_extra = "LEFT JOIN PackageVotes - ON (PackageBases.ID = PackageVotes.PackageBaseID AND PackageVotes.UsersID = $myuid) - LEFT JOIN PackageNotifications - ON (PackageBases.ID = PackageNotifications.PackageBaseID AND PackageNotifications.UserID = $myuid) "; - } else { - $q_from_extra = ""; - } - - $q_where = 'WHERE PackageBases.PackagerUID IS NOT NULL '; - - if (isset($params['K'])) { - if (isset($params["SeB"]) && $params["SeB"] == "m") { - /* Search by maintainer. */ - $q_where .= "AND Users.Username = " . $dbh->quote($params['K']) . " "; - } - elseif (isset($params["SeB"]) && $params["SeB"] == "c") { - /* Search by co-maintainer. */ - $q_where .= "AND EXISTS (SELECT * FROM PackageComaintainers "; - $q_where .= "INNER JOIN Users ON Users.ID = PackageComaintainers.UsersID "; - $q_where .= "WHERE PackageComaintainers.PackageBaseID = PackageBases.ID "; - $q_where .= "AND Users.Username = " . $dbh->quote($params['K']) . ")"; - } - elseif (isset($params["SeB"]) && $params["SeB"] == "M") { - /* Search by maintainer and co-maintainer. */ - $q_where .= "AND (Users.Username = " . $dbh->quote($params['K']) . " "; - $q_where .= "OR EXISTS (SELECT * FROM PackageComaintainers "; - $q_where .= "INNER JOIN Users ON Users.ID = PackageComaintainers.UsersID "; - $q_where .= "WHERE PackageComaintainers.PackageBaseID = PackageBases.ID "; - $q_where .= "AND Users.Username = " . $dbh->quote($params['K']) . "))"; - } - elseif (isset($params["SeB"]) && $params["SeB"] == "s") { - /* Search by submitter. */ - $q_where .= "AND SubmitterUID = " . intval(uid_from_username($params['K'])) . " "; - } - elseif (isset($params["SeB"]) && $params["SeB"] == "n") { - /* Search by name. */ - $K = "%" . addcslashes($params['K'], '%_') . "%"; - $q_where .= "AND (Packages.Name LIKE " . $dbh->quote($K) . ") "; - } - elseif (isset($params["SeB"]) && $params["SeB"] == "b") { - /* Search by package base name. */ - $K = "%" . addcslashes($params['K'], '%_') . "%"; - $q_where .= "AND (PackageBases.Name LIKE " . $dbh->quote($K) . ") "; - } - elseif (isset($params["SeB"]) && $params["SeB"] == "k") { - /* Search by name. */ - $q_where .= construct_keyword_search($dbh, $params['K'], false, true); - } - elseif (isset($params["SeB"]) && $params["SeB"] == "N") { - /* Search by name (exact match). */ - $q_where .= "AND (Packages.Name = " . $dbh->quote($params['K']) . ") "; - } - elseif (isset($params["SeB"]) && $params["SeB"] == "B") { - /* Search by package base name (exact match). */ - $q_where .= "AND (PackageBases.Name = " . $dbh->quote($params['K']) . ") "; - } - else { - /* Keyword search (default). */ - $q_where .= construct_keyword_search($dbh, $params['K'], true, true); - } - } - - if (isset($params["do_Orphans"])) { - $q_where .= "AND MaintainerUID IS NULL "; - } - - if (isset($params['outdated'])) { - if ($params['outdated'] == 'on') { - $q_where .= "AND OutOfDateTS IS NOT NULL "; - } - elseif ($params['outdated'] == 'off') { - $q_where .= "AND OutOfDateTS IS NULL "; - } - } - - $order = (isset($params["SO"]) && $params["SO"] == 'd') ? 'DESC' : 'ASC'; - - $q_sort = "ORDER BY "; - $sort_by = isset($params["SB"]) ? $params["SB"] : ''; - switch ($sort_by) { - case 'v': - $q_sort .= "NumVotes " . $order . ", "; - break; - case 'p': - $q_sort .= "Popularity " . $order . ", "; - break; - case 'w': - if ($SID) { - $q_sort .= "Voted " . $order . ", "; - } - break; - case 'o': - if ($SID) { - $q_sort .= "Notify " . $order . ", "; - } - break; - case 'm': - $q_sort .= "Maintainer " . $order . ", "; - break; - case 'l': - $q_sort .= "ModifiedTS " . $order . ", "; - break; - case 'a': - /* For compatibility with old search links. */ - $q_sort .= "-ModifiedTS " . $order . ", "; - break; - default: - break; - } - $q_sort .= " Packages.Name " . $order . " "; - - $q_limit = "LIMIT ".$params["PP"]." OFFSET ".$params["O"]; - - $q = $q_select . $q_from . $q_from_extra . $q_where . $q_sort . $q_limit; - $q_total = "SELECT COUNT(*) " . $q_from . $q_where; - - $result = $dbh->query($q); - $result_t = $dbh->query($q_total); - if ($result_t) { - $row = $result_t->fetch(PDO::FETCH_NUM); - $total = min($row[0], 2500); - } else { - $total = 0; - } - - if ($result && $total > 0) { - if (isset($params["SO"]) && $params["SO"] == "d"){ - $SO_next = "a"; - } - else { - $SO_next = "d"; - } - } - - /* Calculate the results to use. */ - $first = $params['O'] + 1; - - /* Calculation of pagination links. */ - $per_page = ($params['PP'] > 0) ? $params['PP'] : 50; - $current = ceil($first / $per_page); - $pages = ceil($total / $per_page); - $templ_pages = array(); - - if ($current > 1) { - $templ_pages['« ' . __('First')] = 0; - $templ_pages['‹ ' . __('Previous')] = ($current - 2) * $per_page; - } - - if ($current - 5 > 1) - $templ_pages["..."] = false; - - for ($i = max($current - 5, 1); $i <= min($pages, $current + 5); $i++) { - $templ_pages[$i] = ($i - 1) * $per_page; - } - - if ($current + 5 < $pages) - $templ_pages["... "] = false; - - if ($current < $pages) { - $templ_pages[__('Next') . ' ›'] = $current * $per_page; - $templ_pages[__('Last') . ' »'] = ($pages - 1) * $per_page; - } - - $searchresults = array(); - if ($result) { - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $searchresults[] = $row; - } - } - - include('pkg_search_results.php'); - - return $total; -} - -/** - * Construct the WHERE part of the sophisticated keyword search - * - * @param handle $dbh Database handle - * @param string $keywords The search term - * @param bool $namedesc Search name and description fields - * @param bool $keyword Search packages with a matching PackageBases.Keyword - * - * @return string WHERE part of the SQL clause - */ -function construct_keyword_search($dbh, $keywords, $namedesc, $keyword=false) { - $count = 0; - $where_part = ""; - $q_keywords = ""; - $op = ""; - - foreach (str_getcsv($keywords, ' ') as $term) { - if ($term == "") { - continue; - } - if ($count > 0 && strtolower($term) == "and") { - $op = "AND "; - continue; - } - if ($count > 0 && strtolower($term) == "or") { - $op = "OR "; - continue; - } - if ($count > 0 && strtolower($term) == "not") { - $op .= "NOT "; - continue; - } - - $term = "%" . addcslashes($term, '%_') . "%"; - $q_keywords .= $op . " ("; - $q_keywords .= "Packages.Name LIKE " . $dbh->quote($term) . " "; - if ($namedesc) { - $q_keywords .= "OR Description LIKE " . $dbh->quote($term) . " "; - } - - if ($keyword) { - $q_keywords .= "OR EXISTS (SELECT * FROM PackageKeywords WHERE "; - $q_keywords .= "PackageKeywords.PackageBaseID = Packages.PackageBaseID AND "; - $q_keywords .= "PackageKeywords.Keyword LIKE " . $dbh->quote($term) . ")) "; - } else { - $q_keywords .= ") "; - } - - $count++; - if ($count >= 20) { - break; - } - $op = "AND "; - } - - if (!empty($q_keywords)) { - $where_part = "AND (" . $q_keywords . ") "; - } - - return $where_part; -} - -/** - * Determine if a POST string has been sent by a visitor - * - * @param string $action String to check has been sent via POST - * - * @return bool True if the POST string was used, otherwise false - */ -function current_action($action) { - return (isset($_POST['action']) && $_POST['action'] == $action) || - isset($_POST[$action]); -} - -/** - * Determine if sent IDs are valid integers - * - * @param array $ids IDs to validate - * - * @return array All sent IDs that are valid integers - */ -function sanitize_ids($ids) { - $new_ids = array(); - foreach ($ids as $id) { - $id = intval($id); - if ($id > 0) { - $new_ids[] = $id; - } - } - return $new_ids; -} - -/** - * Determine package information for latest package - * - * @param int $numpkgs Number of packages to get information on - * - * @return array $packages Package info for the specified number of recent packages - */ -function latest_pkgs($numpkgs, $orderBy='SubmittedTS') { - $dbh = DB::connect(); - - $q = "SELECT Packages.*, MaintainerUID, SubmittedTS, ModifiedTS "; - $q.= "FROM Packages LEFT JOIN PackageBases ON "; - $q.= "PackageBases.ID = Packages.PackageBaseID "; - $q.= "ORDER BY " . $orderBy . " DESC "; - $q.= "LIMIT " . intval($numpkgs); - $result = $dbh->query($q); - - $packages = array(); - if ($result) { - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $packages[] = $row; - } - } - - return $packages; -} - -/** - * Determine package information for latest modified packages - * - * @param int $numpkgs Number of packages to get information on - * - * @return array $packages Package info for the specified number of recently modified packages - */ -function latest_modified_pkgs($numpkgs) { - return latest_pkgs($numpkgs, 'ModifiedTS'); -} diff --git a/web/lib/pkgreqfuncs.inc.php b/web/lib/pkgreqfuncs.inc.php deleted file mode 100644 index 7fce307c..00000000 --- a/web/lib/pkgreqfuncs.inc.php +++ /dev/null @@ -1,260 +0,0 @@ -query($q)->fetchColumn(); -} - -/** - * Get a list of all package requests - * - * @param int $offset The index of the first request to return - * @param int $limit The maximum number of requests to return - * @param int $uid Only return packages affecting the given user - * @param int $from Do not return packages older than the given date - * - * @return array List of package requests with details - */ -function pkgreq_list($offset, $limit, $uid=false, $from=false) { - $dbh = DB::connect(); - - $q = "SELECT PackageRequests.ID, "; - $q.= "PackageRequests.PackageBaseID AS BaseID, "; - $q.= "PackageRequests.PackageBaseName AS Name, "; - $q.= "PackageRequests.MergeBaseName AS MergeInto, "; - $q.= "RequestTypes.Name AS Type, PackageRequests.Comments, "; - $q.= "Users.Username AS User, PackageRequests.RequestTS, "; - $q.= "PackageRequests.Status, PackageRequests.Status = 0 AS Open "; - $q.= "FROM PackageRequests INNER JOIN RequestTypes ON "; - $q.= "RequestTypes.ID = PackageRequests.ReqTypeID "; - $q.= "INNER JOIN Users ON Users.ID = PackageRequests.UsersID "; - - if ($uid || $from) { - $q.= "WHERE "; - if ($uid) { - $q.= "(PackageRequests.UsersID = " . intval($uid). " "; - $q.= "OR Users.ID = " . intval($uid) . ") AND "; - } - if ($from) { - $q.= "RequestTS >= " . intval($from). " "; - } - } - - $q.= "ORDER BY Open DESC, RequestTS DESC "; - $q.= "LIMIT " . $limit . " OFFSET " . $offset; - - return $dbh->query($q)->fetchAll(); -} - -/** - * Get a list of all open package requests belonging to a certain package base - * - * @param int $baseid The package base ID to retrieve requests for - * @param int $type The type of requests to obtain - * - * @return array List of package request IDs - */ -function pkgreq_by_pkgbase($baseid, $type=false) { - $dbh = DB::connect(); - - $q = "SELECT PackageRequests.ID "; - $q.= "FROM PackageRequests INNER JOIN RequestTypes ON "; - $q.= "RequestTypes.ID = PackageRequests.ReqTypeID "; - $q.= "WHERE PackageRequests.Status = 0 "; - $q.= "AND PackageRequests.PackageBaseID = " . intval($baseid); - - if ($type) { - $q .= " AND RequestTypes.Name = " . $dbh->quote($type); - } - - return $dbh->query($q)->fetchAll(PDO::FETCH_COLUMN, 0); -} - -/** - * Obtain the package base that belongs to a package request. - * - * @param int $id Package request ID to retrieve the package base for - * - * @return int The name of the corresponding package base - */ -function pkgreq_get_pkgbase_name($id) { - $dbh = DB::connect(); - - $q = "SELECT PackageBaseName FROM PackageRequests "; - $q.= "WHERE ID = " . intval($id); - $result = $dbh->query($q); - return $result->fetch(PDO::FETCH_COLUMN, 0); -} - -/** - * Obtain the email address of the creator of a package request - * - * @param int $id Package request ID to retrieve the creator for - * - * @return int The email address of the creator - */ -function pkgreq_get_creator_email($id) { - $dbh = DB::connect(); - - $q = "SELECT Email FROM Users INNER JOIN PackageRequests "; - $q.= "ON Users.ID = PackageRequests.UsersID "; - $q.= "WHERE PackageRequests.ID = " . intval($id); - $result = $dbh->query($q); - return $result->fetch(PDO::FETCH_COLUMN, 0); -} - -/** - * File a deletion/orphan request against a package base - * - * @param string $ids The package base IDs to file the request against - * @param string $type The type of the request - * @param string $merge_into The target of a merge operation - * @param string $comments The comments to be added to the request - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgreq_file($ids, $type, $merge_into, $comments) { - if (!has_credential(CRED_PKGREQ_FILE)) { - return array(false, __("You must be logged in to file package requests.")); - } - - /* Ignore merge target for non-merge requests. */ - if ($type !== 'merge') { - $merge_into = ''; - } - - if (!empty($merge_into) && !preg_match("/^[a-z0-9][a-z0-9\.+_-]*$/D", $merge_into)) { - return array(false, __("Invalid name: only lowercase letters are allowed.")); - } - - if (!empty($merge_into) && !pkgbase_from_name($merge_into)) { - return array(false, __("Cannot find package to merge votes and comments into.")); - } - - if (empty($comments)) { - return array(false, __("The comment field must not be empty.")); - } - - $dbh = DB::connect(); - $uid = uid_from_sid($_COOKIE["AURSID"]); - - /* TODO: Allow for filing multiple requests at once. */ - $base_id = intval($ids[0]); - $pkgbase_name = pkgbase_name_from_id($base_id); - - if ($merge_into == $pkgbase_name) { - return array(false, __("Cannot merge a package base with itself.")); - } - - $q = "SELECT ID FROM RequestTypes WHERE Name = " . $dbh->quote($type); - $result = $dbh->query($q); - if ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $type_id = $row['ID']; - } else { - return array(false, __("Invalid request type.")); - } - - $q = "INSERT INTO PackageRequests "; - $q.= "(ReqTypeID, PackageBaseID, PackageBaseName, MergeBaseName, "; - $q.= "UsersID, Comments, ClosureComment, RequestTS) VALUES (" . $type_id . ", "; - $q.= $base_id . ", " . $dbh->quote($pkgbase_name) . ", "; - $q.= $dbh->quote($merge_into) . ", " . $uid . ", "; - $q.= $dbh->quote($comments) . ", '', " . strval(time()) . ")"; - $dbh->exec($q); - $request_id = $dbh->lastInsertId(); - - /* Send e-mail notifications. */ - $params = array('request-open', $uid, $request_id, $type, $base_id); - if ($type === 'merge') { - $params[] = $merge_into; - } - notify($params); - - $auto_orphan_age = config_get('options', 'auto_orphan_age'); - $auto_delete_age = config_get('options', 'auto_delete_age'); - $details = pkgbase_get_details($base_id); - if ($type == 'orphan' && $details['OutOfDateTS'] > 0 && - time() - $details['OutOfDateTS'] >= $auto_orphan_age && - $auto_orphan_age > 0) { - /* - * Close package request. NOTE: This needs to happen *before* - * the actual disown operation. Otherwise, the former - * maintainer will not be included in the Cc list of the - * request notification email. - */ - $out_of_date_time = date("Y-m-d", intval($details["OutOfDateTS"])); - pkgreq_close($request_id, "accepted", - "The package base has been flagged out-of-date " . - "since " . $out_of_date_time . ".", true); - $q = "UPDATE PackageBases SET MaintainerUID = NULL "; - $q.= "WHERE ID = " . $base_id; - $dbh->exec($q); - } else if ($type == 'deletion' && $details['MaintainerUID'] == $uid && - $details['SubmittedTS'] > 0 && $auto_delete_age > 0 && - time() - $details['SubmittedTS'] <= $auto_delete_age) { - /* - * Close package request. NOTE: This needs to happen *before* - * the actual deletion operation. Otherwise, the former - * maintainer will not be included in the Cc list of the - * request notification email. - */ - pkgreq_close($request_id, "accepted", - "Deletion of a fresh package requested by its " . - "current maintainer.", true); - pkgbase_delete(array($base_id), NULL, NULL, true); - } - - return array(true, __("Added request successfully.")); -} - -/** - * Close a deletion/orphan request - * - * @param int $id The package request to close - * @param string $reason Whether the request was accepted or rejected - * @param string $comments Comments to be added to the notification email - * @param boolean $auto_close (optional) Whether the request is auto-closed - * - * @return array Tuple of success/failure indicator and error message - */ -function pkgreq_close($id, $reason, $comments, $auto_close=false) { - switch ($reason) { - case 'accepted': - $status = 2; - break; - case 'rejected': - $status = 3; - break; - default: - return array(false, __("Invalid reason.")); - } - - $dbh = DB::connect(); - $id = intval($id); - $uid = $auto_close ? 0 : uid_from_sid($_COOKIE["AURSID"]); - - if (!$auto_close && !has_credential(CRED_PKGREQ_CLOSE)) { - return array(false, __("Only TUs and developers can close requests.")); - } - - $q = "UPDATE PackageRequests SET Status = " . intval($status) . ", "; - $q.= "ClosedTS = " . strval(time()) . ", "; - $q.= "ClosedUID = " . ($uid == 0 ? "NULL" : intval($uid)) . ", "; - $q.= "ClosureComment = " . $dbh->quote($comments) . " "; - $q.= "WHERE ID = " . intval($id); - $dbh->exec($q); - - /* Send e-mail notifications. */ - notify(array('request-close', $uid, $id, $reason)); - - return array(true, __("Request closed successfully.")); -} diff --git a/web/lib/routing.inc.php b/web/lib/routing.inc.php deleted file mode 100644 index 73c667d2..00000000 --- a/web/lib/routing.inc.php +++ /dev/null @@ -1,85 +0,0 @@ - 'home.php', - '/index.php' => 'home.php', - '/packages' => 'packages.php', - '/pkgbase' => 'pkgbase.php', - '/requests' => 'pkgreq.php', - '/register' => 'register.php', - '/account' => 'account.php', - '/accounts' => 'account.php', - '/login' => 'login.php', - '/logout' => 'logout.php', - '/passreset' => 'passreset.php', - '/rpc' => 'rpc.php', - '/rss/modified' => 'modified-rss.php', - '/rss' => 'rss.php', - '/tos' => 'tos.php', - '/tu' => 'tu.php', - '/addvote' => 'addvote.php', -); - -$PKG_PATH = '/packages'; -$PKGBASE_PATH = '/pkgbase'; -$PKGREQ_PATH = '/requests'; -$USER_PATH = '/account'; - -function get_route($path) { - global $ROUTES; - - $path = rtrim($path, '/'); - if (isset($ROUTES[$path])) { - return $ROUTES[$path]; - } else { - return NULL; - } -} - -function get_uri($path, $absolute=false) { - if ($absolute) { - return rtrim(aur_location(), '/') . $path; - } else { - return $path; - } -} - -function get_pkg_route() { - global $PKG_PATH; - return $PKG_PATH; -} - -function get_pkgbase_route() { - global $PKGBASE_PATH; - return $PKGBASE_PATH; -} - -function get_pkgreq_route() { - global $PKGREQ_PATH; - return $PKGREQ_PATH; -} - -function get_pkg_uri($pkgname, $absolute=false) { - global $PKG_PATH; - $path = $PKG_PATH . '/' . urlencode($pkgname) . '/'; - return get_uri($path, $absolute); -} - -function get_pkgbase_uri($pkgbase_name, $absolute=false) { - global $PKGBASE_PATH; - $path = $PKGBASE_PATH . '/' . urlencode($pkgbase_name) . '/'; - return get_uri($path, $absolute); -} - -function get_user_route() { - global $USER_PATH; - return $USER_PATH; -} - -function get_user_uri($username, $absolute=false) { - global $USER_PATH; - $path = $USER_PATH . '/' . urlencode($username) . '/'; - return get_uri($path, $absolute); -} diff --git a/web/lib/stats.inc.php b/web/lib/stats.inc.php deleted file mode 100644 index f5692f96..00000000 --- a/web/lib/stats.inc.php +++ /dev/null @@ -1,108 +0,0 @@ -query($q); - - $newest_packages = new ArrayObject(); - if ($result) { - while ($row = $result->fetch(PDO::FETCH_ASSOC)) { - $newest_packages->append($row); - } - set_cache_value($key, $newest_packages); - } - } - include('stats/updates_table.php'); -} - -/** - * Display a user's statistics table - * - * @param string $userid The user ID of the person to get package statistics for - * - * @return void - */ -function user_table($userid) { - $base_q = "SELECT COUNT(*) FROM PackageBases "; - $base_q.= "WHERE MaintainerUID = " . $userid . " "; - $base_q.= "AND PackagerUID IS NOT NULL"; - - $user_pkg_count = db_cache_value($base_q, 'user_pkg_count:' . $userid); - - $q = "SELECT COUNT(*) FROM PackageBases "; - $q.= "WHERE OutOfDateTS IS NOT NULL "; - $q.= "AND MaintainerUID = " . $userid . " "; - $q.= "AND PackagerUID IS NOT NULL"; - - $flagged_outdated = db_cache_value($q, 'user_flagged_outdated:' . $userid); - - include('stats/user_table.php'); -} - -/** - * Display the general package statistics table - * - * @return void - */ -function general_stats_table() { - # AUR statistics - $q = "SELECT COUNT(*) FROM PackageBases WHERE PackagerUID IS NOT NULL"; - $pkg_count = db_cache_value($q, 'pkg_count'); - - $q = "SELECT COUNT(*) FROM PackageBases "; - $q.= "WHERE MaintainerUID IS NULL "; - $q.= "AND PackagerUID IS NOT NULL"; - $orphan_count = db_cache_value($q, 'orphan_count'); - - $q = "SELECT count(*) FROM Users"; - $user_count = db_cache_value($q, 'user_count'); - - $q = "SELECT count(*) FROM Users,AccountTypes WHERE Users.AccountTypeID = AccountTypes.ID AND (AccountTypes.AccountType = 'Trusted User' OR AccountTypes.AccountType = 'Trusted User & Developer')"; - $tu_count = db_cache_value($q, 'tu_count'); - - $targstamp = intval(strtotime("-7 days")); - $yearstamp = intval(strtotime("-1 year")); - - $q = "SELECT COUNT(*) FROM PackageBases "; - $q.= "WHERE SubmittedTS >= $targstamp "; - $q.= "AND PackagerUID IS NOT NULL"; - $add_count = db_cache_value($q, 'add_count'); - - /* - * A package whose last modification time differs less than an hour - * from the initial submission time is considered new. - */ - - $q = "SELECT COUNT(*) FROM PackageBases "; - $q.= "WHERE ModifiedTS >= $targstamp "; - $q.= "AND ModifiedTS - SubmittedTS >= 3600 "; - $q.= "AND PackagerUID IS NOT NULL"; - $update_count = db_cache_value($q, 'update_count'); - - $q = "SELECT COUNT(*) FROM PackageBases "; - $q.= "WHERE ModifiedTS >= $yearstamp "; - $q.= "AND ModifiedTS - SubmittedTS >= 3600 "; - $q.= "AND PackagerUID IS NOT NULL"; - $update_year_count = db_cache_value($q, 'update_year_count'); - - $q = "SELECT COUNT(*) FROM PackageBases "; - $q.= "WHERE ModifiedTS - SubmittedTS < 3600 "; - $q.= "AND PackagerUID IS NOT NULL"; - $never_update_count = db_cache_value($q, 'never_update_count'); - - include('stats/general_stats_table.php'); -} diff --git a/web/lib/streams.php b/web/lib/streams.php deleted file mode 100644 index 00cf6cc5..00000000 --- a/web/lib/streams.php +++ /dev/null @@ -1,167 +0,0 @@ -. - - This file is part of PHP-gettext. - - PHP-gettext is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - PHP-gettext is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with PHP-gettext; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - -*/ - - - // Simple class to wrap file streams, string streams, etc. - // seek is essential, and it should be byte stream -class StreamReader { - // should return a string [FIXME: perhaps return array of bytes?] - function read($bytes) { - return false; - } - - // should return new position - function seekto($position) { - return false; - } - - // returns current position - function currentpos() { - return false; - } - - // returns length of entire stream (limit for seekto()s) - function length() { - return false; - } -}; - -class StringReader { - var $_pos; - var $_str; - - function __construct($str='') { - $this->_str = $str; - $this->_pos = 0; - } - - function read($bytes) { - $data = substr($this->_str, $this->_pos, $bytes); - $this->_pos += $bytes; - if (strlen($this->_str)<$this->_pos) - $this->_pos = strlen($this->_str); - - return $data; - } - - function seekto($pos) { - $this->_pos = $pos; - if (strlen($this->_str)<$this->_pos) - $this->_pos = strlen($this->_str); - return $this->_pos; - } - - function currentpos() { - return $this->_pos; - } - - function length() { - return strlen($this->_str); - } - -}; - - -class FileReader { - var $_pos; - var $_fd; - var $_length; - - function __construct($filename) { - if (file_exists($filename)) { - - $this->_length=filesize($filename); - $this->_pos = 0; - $this->_fd = fopen($filename,'rb'); - if (!$this->_fd) { - $this->error = 3; // Cannot read file, probably permissions - return false; - } - } else { - $this->error = 2; // File doesn't exist - return false; - } - } - - function read($bytes) { - if ($bytes) { - fseek($this->_fd, $this->_pos); - - // PHP 5.1.1 does not read more than 8192 bytes in one fread() - // the discussions at PHP Bugs suggest it's the intended behaviour - $data = ''; - while ($bytes > 0) { - $chunk = fread($this->_fd, $bytes); - $data .= $chunk; - $bytes -= strlen($chunk); - } - $this->_pos = ftell($this->_fd); - - return $data; - } else return ''; - } - - function seekto($pos) { - fseek($this->_fd, $pos); - $this->_pos = ftell($this->_fd); - return $this->_pos; - } - - function currentpos() { - return $this->_pos; - } - - function length() { - return $this->_length; - } - - function close() { - fclose($this->_fd); - } - -}; - -// Preloads entire file in memory first, then creates a StringReader -// over it (it assumes knowledge of StringReader internals) -class CachedFileReader extends StringReader { - function __construct($filename) { - if (file_exists($filename)) { - - $length=filesize($filename); - $fd = fopen($filename,'rb'); - - if (!$fd) { - $this->error = 3; // Cannot read file, probably permissions - return false; - } - $this->_str = fread($fd, $length); - fclose($fd); - - } else { - $this->error = 2; // File doesn't exist - return false; - } - } -}; - - -?> diff --git a/web/lib/timezone.inc.php b/web/lib/timezone.inc.php deleted file mode 100644 index 949f846d..00000000 --- a/web/lib/timezone.inc.php +++ /dev/null @@ -1,63 +0,0 @@ - Displayed Description - */ -function generate_timezone_list() { - $php_timezones = DateTimeZone::listIdentifiers(DateTimeZone::ALL); - - $offsets = array(); - foreach ($php_timezones as $timezone) { - $tz = new DateTimeZone($timezone); - $offset = $tz->getOffset(new DateTime()); - $offsets[$timezone] = "(UTC" . ($offset < 0 ? "-" : "+") . gmdate("H:i", abs($offset)) . - ") " . $timezone; - } - - asort($offsets); - return $offsets; -} - -/** - * Set the timezone for the user. - * - * @return null - */ -function set_tz() { - $timezones = generate_timezone_list(); - $update_cookie = false; - - if (isset($_COOKIE["AURTZ"])) { - $timezone = $_COOKIE["AURTZ"]; - } elseif (isset($_COOKIE["AURSID"])) { - $dbh = DB::connect(); - $q = "SELECT Timezone FROM Users, Sessions "; - $q .= "WHERE Users.ID = Sessions.UsersID "; - $q .= "AND Sessions.SessionID = "; - $q .= $dbh->quote($_COOKIE["AURSID"]); - $result = $dbh->query($q); - - if ($result) { - $timezone = $result->fetchColumn(0); - if (!$timezone) { - unset($timezone); - } - } - - $update_cookie = true; - } - - if (!isset($timezone) || !array_key_exists($timezone, $timezones)) { - $timezone = config_get("options", "default_timezone"); - } - date_default_timezone_set($timezone); - - if ($update_cookie) { - $timeout = intval(config_get("options", "persistent_cookie_timeout")); - $cookie_time = time() + $timeout; - setcookie("AURTZ", $timezone, $cookie_time, "/"); - } -} diff --git a/web/lib/translator.inc.php b/web/lib/translator.inc.php deleted file mode 100644 index cbed1274..00000000 --- a/web/lib/translator.inc.php +++ /dev/null @@ -1,139 +0,0 @@ -", ""); - -include_once("confparser.inc.php"); -include_once('DB.class.php'); -include_once('gettext.php'); -include_once('streams.php'); - -global $streamer, $l10n; - -# Languages we have translations for -$SUPPORTED_LANGS = array( - "ar" => "العربية", - "ast" => "Asturianu", - "ca" => "Català", - "cs" => "Český", - "da" => "Dansk", - "de" => "Deutsch", - "en" => "English", - "el" => "Ελληνικά", - "es" => "Español", - "es_419" => "Español (Latinoamérica)", - "fi" => "Suomi", - "fr" => "Français", - "he" => "עברית", - "hr" => "Hrvatski", - "hu" => "Magyar", - "it" => "Italiano", - "ja" => "日本語", - "nb" => "Norsk", - "nl" => "Nederlands", - "pl" => "Polski", - "pt_BR" => "Português (Brasil)", - "pt_PT" => "Português (Portugal)", - "ro" => "Română", - "ru" => "Русский", - "sk" => "Slovenčina", - "sr" => "Srpski", - "tr" => "Türkçe", - "uk" => "Українська", - "zh_CN" => "简体中文", - "zh_TW" => "正體中文" -); - -function __() { - global $LANG; - global $l10n; - - # Create the translation. - $args = func_get_args(); - - # First argument is always string to be translated - $tag = array_shift($args); - - # Translate using gettext_reader initialized before. - $translated = $l10n->translate($tag); - $translated = htmlspecialchars($translated, ENT_QUOTES); - - # Subsequent arguments are strings to be formatted - if (count($args) > 0) { - $translated = vsprintf($translated, $args); - } - - return $translated; -} - -function _n($msgid1, $msgid2, $n) { - global $l10n; - - $translated = sprintf($l10n->ngettext($msgid1, $msgid2, $n), $n); - return htmlspecialchars($translated, ENT_QUOTES); -} - -# set up the visitor's language -# -function set_lang() { - global $LANG; - global $SUPPORTED_LANGS; - global $streamer, $l10n; - - $update_cookie = 0; - if (isset($_POST['setlang'])) { - # visitor is requesting a language change - # - $LANG = $_POST['setlang']; - $update_cookie = 1; - - } elseif (isset($_COOKIE['AURLANG'])) { - # If a cookie is set, use that - # - $LANG = $_COOKIE['AURLANG']; - - } elseif (isset($_COOKIE["AURSID"])) { - # No language but a session; use default lang preference - # - $dbh = DB::connect(); - $q = "SELECT LangPreference FROM Users, Sessions "; - $q.= "WHERE Users.ID = Sessions.UsersID "; - $q.= "AND Sessions.SessionID = "; - $q.= $dbh->quote($_COOKIE["AURSID"]); - $result = $dbh->query($q); - - if ($result) { - $LANG = $result->fetchColumn(0); - if (!$LANG) { - unset($LANG); - } - } - $update_cookie = 1; - } - - # Set $LANG to default if nothing is valid. - if (!isset($LANG) || !array_key_exists($LANG, $SUPPORTED_LANGS)) { - $LANG = config_get('options', 'default_lang'); - } - - if ($update_cookie) { - $timeout = intval(config_get('options', 'persistent_cookie_timeout')); - $cookie_time = time() + $timeout; - setcookie("AURLANG", $LANG, $cookie_time, "/"); - } - - $localedir = config_get('options', 'localedir'); - $streamer = new FileReader($localedir . '/' . $LANG . - '/LC_MESSAGES/aurweb.mo'); - $l10n = new gettext_reader($streamer, true); - - return; -} diff --git a/web/lib/version.inc.php b/web/lib/version.inc.php deleted file mode 100644 index 27eef718..00000000 --- a/web/lib/version.inc.php +++ /dev/null @@ -1,3 +0,0 @@ - - ' . htmlspecialchars($username) . '') ?> -

    -

    - ', '') ?> -

    - -
    -
    - - - -
    -
    -

    - - -

    - -

    - -

    - -

    - " /> -

    -
    -
    diff --git a/web/template/account_details.php b/web/template/account_details.php deleted file mode 100644 index 84f8b9c5..00000000 --- a/web/template/account_details.php +++ /dev/null @@ -1,93 +0,0 @@ - - - - -
    -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    - " . __("hidden") . ""; - else: - ?> - "> - -
    " rel="nofollow">
    - -
    format('Y-m-d') ?>
    - -
    Links:
      -
    • - -
    • - - -
    • - -
    -
    diff --git a/web/template/account_edit_form.php b/web/template/account_edit_form.php deleted file mode 100644 index 4ce6b875..00000000 --- a/web/template/account_edit_form.php +++ /dev/null @@ -1,222 +0,0 @@ - -

    - ', '') ?> - ', '') ?> - ', '') ?> -

    - -
    - - - -
    - - - - - -
    -
    -

    - - () -

    -

    - -

    - -

    - - -

    - -

    - - - - - - -

    - - - -

    - - /> -

    - - -

    - - () -

    -

    - -

    - -

    - - /> -

    -

    - -

    - -

    - - -

    -

    - - - - " . __("Hide Email Address") . "") ?> - -

    - -

    - - -

    - -

    - - -

    - -

    - - -

    - -

    - - -

    - -

    - - -

    -

    - - -

    -
    - - -
    - -

    - - -

    - -

    - - -

    -
    - - -
    - -

    - - -

    -
    - -
    - : -

    - - /> -

    -

    - - /> -

    -

    - - /> -

    -
    - -
    - - -

    - - -

    - - -

    - - () - -

    - -
    - -
    -

    - - - " />   - - " />   - - " /> -

    -
    -
    diff --git a/web/template/account_search_results.php b/web/template/account_search_results.php deleted file mode 100644 index 0f7eb7a4..00000000 --- a/web/template/account_search_results.php +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - $row): ?> - - - - - - - - - - - -
    "> - - - - "> - -   - -
    - - - - - - -
    -
    -
    - - - $ind): - ?> - - - " /> -
    -
    -
    -
    -
    - - - $ind): - ?> - - - -->" /> -
    -
    -
    - -

    - -

    - - diff --git a/web/template/comaintainers_form.php b/web/template/comaintainers_form.php deleted file mode 100644 index 79e2b52c..00000000 --- a/web/template/comaintainers_form.php +++ /dev/null @@ -1,20 +0,0 @@ -
    -

    :

    -

    - ', htmlspecialchars($pkgbase_name), ''); ?> -

    -
    -
    - -

    - - -

    -

    - " /> -

    -
    -
    -
    - diff --git a/web/template/flag_comment.php b/web/template/flag_comment.php deleted file mode 100644 index 05eeacb2..00000000 --- a/web/template/flag_comment.php +++ /dev/null @@ -1,27 +0,0 @@ -
    -

    -

    - - ', html_format_username($message['Username']), '', - '', htmlspecialchars($pkgbase_name), '', - '', date('Y-m-d', $message['OutOfDateTS']), ''); ?> - - ', htmlspecialchars($pkgbase_name), ''); ?> - -

    - -

    -

    -

    -
    -

    - -

    -

    - " /> -
    -

    -
    - diff --git a/web/template/footer.php b/web/template/footer.php deleted file mode 100644 index 7f97aae0..00000000 --- a/web/template/footer.php +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - diff --git a/web/template/header.php b/web/template/header.php deleted file mode 100644 index afe7a9b6..00000000 --- a/web/template/header.php +++ /dev/null @@ -1,83 +0,0 @@ -'; ?> - - - - AUR (<?= htmlspecialchars($LANG); ?>)<?php if ($title != "") { print " - " . htmlspecialchars($title); } ?> - - - - ' /> - ' /> - - - - - - - - -
    -
    -
    "> -
    -
    - - -
    -
    -
    -
    -
    -
      - -
    • -
    • - -
    • - - -
    • - -
    • -
    • -
    • - -
    • AUR
    • -
    • -
    • - -
    • - -
    • - - -
    -
    - - diff --git a/web/template/pkg_comment_box.php b/web/template/pkg_comment_box.php deleted file mode 100644 index 22f90d4a..00000000 --- a/web/template/pkg_comment_box.php +++ /dev/null @@ -1,4 +0,0 @@ -
    -

    - -
    diff --git a/web/template/pkg_comment_form.php b/web/template/pkg_comment_form.php deleted file mode 100644 index e8a516e3..00000000 --- a/web/template/pkg_comment_form.php +++ /dev/null @@ -1,28 +0,0 @@ -
    -
    -
    - " /> - - - - - -
    -

    - - ', "") ?> -

    -

    - -

    -

    - " /> - - - - - - -

    -
    -
    diff --git a/web/template/pkg_comments.php b/web/template/pkg_comments.php deleted file mode 100644 index ffa9e137..00000000 --- a/web/template/pkg_comments.php +++ /dev/null @@ -1,239 +0,0 @@ - - - - -
    - -
    - -
    -

    - - - - - - - - - - - - -

    - - 1): ?> -

    - $pagestart): ?> - - - - - - - - - - - - -

    - -
    - - $row): ?> - ' . $date_fmtd . ''; - if ($comment_section == "package") { - if ($row['UserName']) { - $user_fmtd = html_format_username($row['UserName']); - $heading = __('%s commented on %s', $user_fmtd, $date_link); - } else { - $heading = __('Anonymous comment on %s', $date_link); - } - } elseif ($comment_section == "account") { - $pkg_uri = '' . htmlspecialchars($row['PackageBaseName']) . ''; - $heading = __('Commented on package %s on %s', $pkg_uri, $date_link); - } - - $is_deleted = $row['DelTS']; - $is_edited = $row['EditedTS']; - $is_pinned = $row['PinnedTS']; - - if ($uid && $is_deleted) { - $date_fmtd = date('Y-m-d H:i', $row['DelTS']); - $heading .= ' ('; - if ($row['DelUserName']) { - $user_fmtd = html_format_username($row['DelUserName']); - $heading .= __('deleted on %s by %s', $date_fmtd, $user_fmtd); - } else { - $heading .= __('deleted on %s', $date_fmtd); - } - $heading .= ')'; - } elseif ($uid && $is_edited) { - $date_fmtd = date('Y-m-d H:i', $row['EditedTS']); - $heading .= ' ('; - if ($row['EditUserName']) { - $user_fmtd = html_format_username($row['EditUserName']); - $heading .= __('edited on %s by %s', $date_fmtd, $user_fmtd); - } else { - $heading .= __('edited on %s', $date_fmtd); - } - $heading .= ')'; - } - - $comment_classes = "comment-header"; - if ($is_deleted) { - $comment_classes .= " comment-deleted"; - } - ?> -

    - - -
    -
    - - - - " /> - -
    -
    - - - -
    -
    - - - - " /> - -
    -
    - - - - <?= __('Edit comment') ?> - - - = 5)): ?> -
    -
    - - - " /> - - " /> - -
    -
    - - - -
    -
    - - - - " /> - -
    -
    - -

    -
    -
    - - - -

    - -

    - -
    -
    - -
    - - diff --git a/web/template/pkg_details.php b/web/template/pkg_details.php deleted file mode 100644 index 25d85b78..00000000 --- a/web/template/pkg_details.php +++ /dev/null @@ -1,317 +0,0 @@ - -
    -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 0): -?> - - - - - - 0): ?> - - - - - - 0): ?> - - - - - - 0): ?> - - - - - - 0): ?> - - - - - - 0): ?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - (, ) - -
    () - -
    - -
    -
    - - - - - "/> - -
    -
    -' . htmlspecialchars($kw) . "\n"; - } -endif; -?> -
    - - - - , - - - - - -
    - - - - , - - - - - -
    - - - - , - - - - - -
    - - - - , - - - - - -
    - - - - , - - - - - -
    = .2 ? 2 : 6) ?>
    - -
    -
    -

    - 0): ?> -
      - $darr): ?> -
    • - -
    - -
    -
    -

    - 0): ?> -
      - $darr): ?> -
    • - -
    - -
    -
    -

    -
    - 0): ?> -
    -
      - $src): ?> -
    • - -
    -
    - -
    -
    - - diff --git a/web/template/pkg_search_form.php b/web/template/pkg_search_form.php deleted file mode 100644 index 3d0cde6c..00000000 --- a/web/template/pkg_search_form.php +++ /dev/null @@ -1,120 +0,0 @@ - __('Name, Description'), - 'n' => __('Name Only'), - 'b' => __('Package Base'), - 'N' => __('Exact Name'), - 'B' => __('Exact Package Base'), - 'k' => __('Keywords'), - 'm' => __('Maintainer'), - 'c' => __('Co-maintainer'), - 'M' => __('Maintainer, Co-maintainer'), - 's' => __('Submitter') -); - -$outdated_flags = array( - '' => __('All'), - 'on' => __('Flagged'), - 'off' => __('Not Flagged') -); - -$sortby = array( - 'n' => __('Name'), - 'v' => __('Votes'), - 'p' => __('Popularity'), - 'w' => __('Voted'), - 'o' => __('Notify'), - 'm' => __('Maintainer'), - 'l' => __('Last modified') -); - -$orderby = array( - 'a' => __('Ascending'), - 'd' => __('Descending') -); - -$per_page = array(50, 100, 250); -?> - - diff --git a/web/template/pkg_search_results.php b/web/template/pkg_search_results.php deleted file mode 100644 index 61335560..00000000 --- a/web/template/pkg_search_results.php +++ /dev/null @@ -1,153 +0,0 @@ -'; - if ($sb) { - echo '' . $title . ''; - } else { - echo $title; - } - if ($hint) { - echo '?'; - } - echo ''; - }; -} else { - $fmtth = function($title, $sb=false, $so=false, $hint=false) { - echo '' . $title . ''; - }; -} - -if (!$result): ?> -

    - -

    - - -
    -

    - - -

    - 1): ?> -

    - $pagestart): ?> - - - - - - - - -

    - -
    - - -
    - - - - - - - - - - - - - - - - - - - - - $row): ?> - - - - - - class="flagged"> - - - - - - - - - - - - -
     
    ]" value="1" />"> - - - - - - - - - - - - - - - - - -
    - - -
    -

    - - -

    - 1): ?> -

    - $pagestart): ?> - - - - - - - - -

    - -
    - - -

    - - - - - - - - " /> -

    - - -
    - diff --git a/web/template/pkgbase_actions.php b/web/template/pkgbase_actions.php deleted file mode 100644 index 3d208328..00000000 --- a/web/template/pkgbase_actions.php +++ /dev/null @@ -1,49 +0,0 @@ - diff --git a/web/template/pkgbase_details.php b/web/template/pkgbase_details.php deleted file mode 100644 index bde29c1c..00000000 --- a/web/template/pkgbase_details.php +++ /dev/null @@ -1,146 +0,0 @@ - -
    -

    - - - - - - - - - 0): -?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - (, ) - -
    () - -
    - -
    -
    - - - - - "/> - -
    -
    -' . htmlspecialchars($kw) . "\n"; - } -endif; -?> -
    = .2 ? 2 : 6) ?>
    - -
    -
    -

    - 0): ?> -
      - $pkg): -?> -
    • - -
    - -
    -
    -
    - - diff --git a/web/template/pkgreq_close_form.php b/web/template/pkgreq_close_form.php deleted file mode 100644 index 6077b325..00000000 --- a/web/template/pkgreq_close_form.php +++ /dev/null @@ -1,32 +0,0 @@ -
    -

    :

    -

    - ', htmlspecialchars($pkgbase_name), ''); ?> -

    -

    - : - -

    -
    -
    - - -

    - - -

    -

    - - -

    -

    - " /> -

    -
    -
    -
    - diff --git a/web/template/pkgreq_form.php b/web/template/pkgreq_form.php deleted file mode 100644 index 9d74093e..00000000 --- a/web/template/pkgreq_form.php +++ /dev/null @@ -1,81 +0,0 @@ -
    -

    :

    -

    - ', htmlspecialchars($pkgbase_name), ''); ?> -

    -
      - -
    • - -
    -
    -
    - - - -

    - - -

    - - -

    - - -

    -

    - - -

    -

    - -

    -

    - -

    -

    - -

    -

    - " /> -

    -
    -
    -
    diff --git a/web/template/pkgreq_results.php b/web/template/pkgreq_results.php deleted file mode 100644 index 1a565c3e..00000000 --- a/web/template/pkgreq_results.php +++ /dev/null @@ -1,129 +0,0 @@ - -

    - - -
    -

    - - -

    - 1): ?> -

    - $pagestart): ?> - - - - - - - - -

    - -
    - - - - - - - - - - - - - - - - $row): ?> - $idle_time); - if (!$due) { - $time_left = $idle_time - (time() - intval($row['RequestTS'])); - if ($time_left > 48 * 3600) { - $time_left_fmt = _n("~%d day left", "~%d days left", round($time_left / (24 * 3600))); - } elseif ($time_left > 3600) { - $time_left_fmt = _n("~%d hour left", "~%d hours left", round($time_left / 3600)); - } else { - $time_left_fmt = __("<1 hour left"); - } - } - ?> - - - - - - - - - - - - - - class="flagged"> - - - - - - - - - - - - - - - - - -
    "> - - - () - - - - - - - - - - - - - () - -
    - - -
    - - -
    -

    - - -

    - 1): ?> -

    - $pagestart): ?> - - - - - - - - -

    - -
    - - diff --git a/web/template/search_accounts_form.php b/web/template/search_accounts_form.php deleted file mode 100644 index f7824a94..00000000 --- a/web/template/search_accounts_form.php +++ /dev/null @@ -1,52 +0,0 @@ -
    -
    -
    - -
    -
    -

    - - -

    -

    - - -

    -

    - - -

    -

    - - -

    -

    - - -

    -

    - - -

    -

    - - -

    -

    - - " />   - " /> -

    -
    -
    diff --git a/web/template/stats/general_stats_table.php b/web/template/stats/general_stats_table.php deleted file mode 100644 index 9dcc3aaf..00000000 --- a/web/template/stats/general_stats_table.php +++ /dev/null @@ -1,36 +0,0 @@ -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    diff --git a/web/template/stats/updates_table.php b/web/template/stats/updates_table.php deleted file mode 100644 index 23a86288..00000000 --- a/web/template/stats/updates_table.php +++ /dev/null @@ -1,19 +0,0 @@ -

    ()

    - -RSS Feed -RSS Feed - - - - getIterator() as $row): ?> - - - - - - -
    - " title=""> - - -
    diff --git a/web/template/stats/user_table.php b/web/template/stats/user_table.php deleted file mode 100644 index e7b00834..00000000 --- a/web/template/stats/user_table.php +++ /dev/null @@ -1,21 +0,0 @@ - - -

    - - - - - - - - - - -
    - - -
    - -
    diff --git a/web/template/template.phps b/web/template/template.phps deleted file mode 100644 index 4f8117c8..00000000 --- a/web/template/template.phps +++ /dev/null @@ -1,20 +0,0 @@ -\n"; - - -html_footer(AURWEB_VERSION); - diff --git a/web/template/tu_details.php b/web/template/tu_details.php deleted file mode 100644 index d739060d..00000000 --- a/web/template/tu_details.php +++ /dev/null @@ -1,123 +0,0 @@ - 0) { - $participation = $total / $active_tus; -} else { - $participation = 0; -} - -if ($yes > $active_tus / 2) { - $vote_accepted = true; -} elseif ($participation > $quorum && $yes > $no) { - $vote_accepted = true; -} else { - $vote_accepted = false; -} -?> -
    -

    - - -

    - -

    - - -

    - : - - - - - N/A - - -
    - -
    - : - - -
    - : - - - - - - - - -

    - -

    - \n", htmlspecialchars($row['Agenda'])) ?> -

    - - - - - - - - - - - - - - - - - - - - - 0): ?> - - - - - -
    - - - - - - %
    -
    - - -
    -

    -
      - -
    • - -
    -
    - - -
    - - -
    -
    - " /> - " /> - " /> - - -
    -
    - - -
    diff --git a/web/template/tu_last_votes_list.php b/web/template/tu_last_votes_list.php deleted file mode 100644 index 6e852581..00000000 --- a/web/template/tu_last_votes_list.php +++ /dev/null @@ -1,36 +0,0 @@ -
    -

    - - - - - - - - - - - - $row): - if ($indx % 2): - $c = "even"; - else: - $c = "odd"; - endif; - $username = username_from_id($row["UserID"]); - ?> - - - - - - -
    - - - -
    -
    diff --git a/web/template/tu_list.php b/web/template/tu_list.php deleted file mode 100644 index 204c89ea..00000000 --- a/web/template/tu_list.php +++ /dev/null @@ -1,82 +0,0 @@ -
    -

    - - -
      -
    • -
    - - - -

    - - - - - - - - - - - - - - - - - - $row): ?> - - - - - - - - - - - - - - -
    - - - - - - - - - - - -
    - -
    -

    - - 0 && $off != 0): - $back = (($off - $limit) <= 0) ? 0 : $off - $limit; ?> - ?off=&by='>‹ - - - - - -

    - -